Skip to content

Turn strava.py chat into a tool-using agent with runlog search#3

Open
sahilds1 wants to merge 5 commits into
mainfrom
2-turn-stravapy-into-an-agent
Open

Turn strava.py chat into a tool-using agent with runlog search#3
sahilds1 wants to merge 5 commits into
mainfrom
2-turn-stravapy-into-an-agent

Conversation

@sahilds1

@sahilds1 sahilds1 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Description

Replace the passive single-shot chat with an agent loop: the conversation now runs llm's tool-calling loop (model.conversation(tools=[...]) + conversation.chain()), so the model can decide mid-conversation to call a tool and act on the result, instead of only answering from the seeded activity JSON.

The agent's first tool is search_runlog, a keyword search over the athlete's running log (RUNLOG.txt, override with --runlog). It lives in a new self-contained strava_tools.py built from pure parse_runlog / search_entries / format_entries helpers, which are the test targets in test_strava_tools.py. Adding a second tool is a one-line change to the tools=[...] list.

format_entries returns self-contained titles + bodies on purpose: with the default OpenAI (Chat Completions) plugin, reasoning state is not preserved across turns or across a tool hop, so the tool's returned text is the only durable state the next turn sees. strava.py carries a comment on this and on the OpenAI Agents SDK as a future extension path.

Also updates SYSTEM_PROMPT to tell the model about the tool and to ground history claims in what it returns, and documents the agent in CLAUDE.md.

Related Issue

Manual Tests

sahildshah•~/github/tiny-tools(2-turn-stravapy-into-an-agent⚡)» uv run strava.py --start 2026-07-11 --end 2026-07-15                            [18:56:15]
No rates present in response headers
Chatting about 4 activities. Ask a question ('exit' or Ctrl-D to quit).

> What are three things  that worked and one thing to improve



Based on the analysis of your recent activities, here are three things that worked and one area for improvement:

### Three Things That Worked:
1. **Consistent Run Performance:**
   - Your average speed across the last three runs was relatively consistent, ranging from 2.23 m/s (approximately 5:58 min/km) to 2.27 m/s (approximately 5:52 min/km) in your runs on July 11 and July 13. This consistency indicates a stable pacing strategy that you have managed to maintain.

2. **Effective Heart Rate Management:**
   - Your average heart rate varied (from 158 bpm to 178 bpm) but is reflective of a well-controlled effort. Keeping your heart rate around 150-180 bpm during runs suggests you're working at a good intensity without overexerting yourself, particularly during longer distances.

3. **Best Efforts Indication:**
   - You've shown impressive best efforts in shorter distances (400m, 1K, mile segments) with good times recorded, indicating that your speed and anaerobic capacity are on the rise. For example, your 400m best effort was completed in 159 seconds during your July 11 run, suggesting strong speed work during your training.

### One Thing to Improve:
- **Elevation Gain and Adaptation:**
  - Your runs have come with minimal elevation gain (e.g., 57.6m on July 11 and 76.6m on July 13), suggesting you might benefit from incorporating hill workouts. Adding elevation can improve strength and power during runs. Focus on gradual increases in hill sessions once a week to enhance your overall performance and adapt your leg muscles to varying terrain.

Incorporating these insights into your training plan can help refine your approach and maximize your performance in future workouts. Let me know if you have any further questions or would like more specific strategies!

>
>
>
> Why didn't I run on  Wednesday July 15 2026
According to the Strava activities data, you did not have any recorded running activity on Wednesday, July 15, 2026. The only activity logged on that date was a weight training session, which lasted 30 minutes (1823 seconds), and there was no mention or record of a running activity.

It's possible that you may have scheduled a rest day, opted for cross-training (like the weight training), or simply missed your run for any number of personal reasons (time constraints, fatigue, or recovery needs).

If you're looking for a more specific reason or pattern in your training, let me know, and I can check your past logs for more context!

> Check my past logsf or more context
The search of your past logs indicates that on **Wednesday, July 15, 2026**, there was a **heat advisory** in effect from 10 AM to PM ET. This weather condition may have influenced your decision to skip running that day, prioritizing safety and avoiding heat-related issues.

Additionally, your last recorded run prior to that was on **Tuesday, July 14, 2026**, where you mentioned kicking at the end of the run, indicating you had a solid training session just before the heat advisory.

In summary, it appears that the combination of the heat advisory and possibly needing a rest day after a solid run contributed to your decision not to run on July 15.

Automated Tests

Documentation

Reviewers

Notes

sahilds1 added 2 commits July 10, 2026 19:31
Replace the passive single-shot chat with an agent loop: the conversation
now runs llm's tool-calling loop (model.conversation(tools=[...]) +
conversation.chain()), so the model can decide mid-conversation to call a
tool and act on the result, instead of only answering from the seeded
activity JSON.

The agent's first tool is search_runlog, a keyword search over the
athlete's running log (RUNLOG.txt, override with --runlog). It lives in a
new self-contained strava_tools.py built from pure parse_runlog /
search_entries / format_entries helpers, which are the test targets in
test_strava_tools.py. Adding a second tool is a one-line change to the
tools=[...] list.

format_entries returns self-contained titles + bodies on purpose: with the
default OpenAI (Chat Completions) plugin, reasoning state is not preserved
across turns or across a tool hop, so the tool's returned text is the only
durable state the next turn sees. strava.py carries a comment on this and
on the OpenAI Agents SDK as a future extension path.

Also updates SYSTEM_PROMPT to tell the model about the tool and to ground
history claims in what it returns, and documents the agent in CLAUDE.md.
Replace search_entries' hand-rolled token-match scorer with scikit-learn's
TfidfVectorizer (fit corpus, cosine-score the query, stable-sort, keep
score > 0, slice to limit). TF-IDF weights terms by rarity, so a match on a
distinctive word (e.g. "fartlek") outranks one on a ubiquitous word like
"run" -- the thing plain counting got wrong. Drops the now-unused _tokenize.

Adds scikit-learn to strava.py's script block and pyproject.toml (standalone
run and pytest are separate environments). Comments record the decisions
(scikit-learn for the simplest code despite the numpy/scipy weight; title text
folded into the document but not up-weighted) and point to BM25 as the next
step, via rank-bm25 or SQLite FTS5.

Tests now cover only the logic we wrote around the vectorizer -- the
empty-corpus guard, the score > 0 filter, and the limit slice -- not
scikit-learn's ranking; drops the ranking and end-to-end glue tests.
@sahilds1 sahilds1 self-assigned this Jul 17, 2026
sahilds1 added 3 commits July 17, 2026 17:57
The `llm`-vs-agent-SDK rationale and the reasoning-state caveat now live
in CLAUDE.md and WORKLOG.md; keep the inline comment to what the code
does. Note at the import why strava_tools is imported as a module —
--runlog rebinds strava_tools.RUNLOG_PATH, which a from-import misses.
RUNLOG.txt delimits entries with "# " headings now, so a title is a
heading line rather than "any non-bullet line" -- which lets a body hold
prose and not just bullets. Split the text on the heading pattern instead
of walking it line by line: re.split's capturing group hands back each
title and body whole, so they go straight into a RunlogEntry with no
sentinel, no buffer, and no flush step.

Make RunlogEntry a frozen dataclass. Nothing depended on its tuple-ness,
and value semantics without structural equality mean an entry won't
compare equal to a bare 2-tuple or shift positional access if a field is
added later.

Move search_runlog to the top of the module, ahead of the helpers it
composes, so the tool itself is what a reader meets first.

Tests: fixtures converted to headings, plus a case for text before the
first heading. Both TODO'd as unrun.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant