Skip to content

Commit 8828164

Browse files
committed
feat: add recipe caching and extraction features
- Introduced a new recipe module for parameterized API-call and SQL pipeline caching. - Implemented recipe extraction from successful agent runs, allowing for reusable templates. - Added support for fuzzy matching of recipes using RapidFuzz. - Updated configuration to enable recipe caching and set cache size. - Enhanced documentation to include new recipe features and usage examples. - Added unit and integration tests for recipe functionalities.
1 parent 567ecb7 commit 8828164

23 files changed

+3321
-204
lines changed

CLAUDE.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ uv sync --group dev
1111

1212
**Run server:**
1313
```bash
14-
uv run api-agent
14+
uv run api-agent # Local dev
15+
# Or direct (no clone): uvx --from git+https://github.com/agoda-com/api-agent api-agent
1516
# Server starts on http://localhost:3000/mcp
1617
```
1718

@@ -71,6 +72,13 @@ docker run -p 3000:3000 -e OPENAI_API_KEY="..." api-agent
7172
- **model.py**: LLM config (OpenAI-compatible)
7273
- **progress.py**: Turn tracking
7374
- **schema_search.py**: Grep-like schema search tool
75+
- **contextvar_utils.py**: Safe ContextVar access helpers
76+
77+
- **api_agent/recipe/**: Parameterized pipeline caching
78+
- **store.py**: `RecipeStore` (LRU in-memory cache, thread-safe)
79+
- **extractor.py**: Extract reusable recipes from agent runs
80+
- **tools.py**: Create dynamic MCP tools from recipes
81+
- **common.py**: Recipe validation, execution, parameter binding
7482

7583
- **api_agent/graphql/**: GraphQL client (httpx)
7684
- **api_agent/rest/**: REST client (httpx) + OpenAPI loader
@@ -104,6 +112,26 @@ Set `X-Poll-Paths` header to enable `poll_until_done` tool:
104112
- Checks `done_field` (dot-path like `"status"`, `"trips.0.isCompleted"`) against `done_value`
105113
- Max 20 polls (configurable), default 3s delay
106114

115+
### Recipes
116+
117+
Caches parameterized API call + SQL pipelines from successful agent runs:
118+
119+
```
120+
Query → Agent executes → Extractor LLM → Recipe stored → Future match → Direct execute
121+
```
122+
123+
- **Storage**: LRU in-memory (default 64 entries, `RECIPE_CACHE_SIZE`)
124+
- **Key**: `(api_id, schema_hash)` - auto-invalidates on schema change
125+
- **Matching**: Fuzzy question similarity via RapidFuzz token matching
126+
- **Templating**:
127+
- GraphQL: `{{param}}` in `query_template`
128+
- REST: `{"$param": "name"}` in `path_params`, `query_params`, `body`
129+
- SQL: `{{param}}` in SQL strings
130+
- **Validation**: Recipe must render back to original execution (roundtrip check)
131+
- **Config**: `ENABLE_RECIPES` (default: True), `RECIPE_CACHE_SIZE` (default: 64)
132+
133+
Key files: `store.py` (LRU cache), `extractor.py` (LLM extraction), `common.py` (execution)
134+
107135
## Testing Notes
108136

109137
Tests use pytest-asyncio. Mock httpx for HTTP calls. See `tests/test_*.py` for patterns.

README.md

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,22 @@ Point at any GraphQL or REST API. Ask questions in natural language. The agent f
1212

1313
**🔒 Safe by default.** Read-only. Mutations blocked unless explicitly allowed.
1414

15+
**🧠 Recipe learning.** Agent learns from successful queries. Ask once, reuse cached pipelines execute instantly without LLM reasoning.
16+
1517
## Quick Start
1618

17-
**1. Run:**
18-
```bash
19-
git clone https://github.com/agoda-com/api-agent.git
20-
cd api-agent
21-
uv sync
22-
OPENAI_API_KEY=your_key uv run python -m api_agent
23-
```
19+
**1. Run (choose one):**
2420

25-
Or with Docker:
2621
```bash
22+
# Direct run (no clone needed)
23+
OPENAI_API_KEY=your_key uvx --from git+https://github.com/agoda-com/api-agent api-agent
24+
25+
# Or clone & run
26+
git clone https://github.com/agoda-com/api-agent.git && cd api-agent
27+
uv sync && OPENAI_API_KEY=your_key uv run api-agent
28+
29+
# Or Docker
30+
git clone https://github.com/agoda-com/api-agent
2731
docker build -t api-agent .
2832
docker run -p 3000:3000 -e OPENAI_API_KEY=your_key api-agent
2933
```
@@ -119,6 +123,8 @@ Tool names auto-generated from URL (e.g., `example_query`). Override with `X-API
119123
| `OPENAI_BASE_URL` | No | https://api.openai.com/v1 | Custom LLM endpoint |
120124
| `API_AGENT_MODEL_NAME` | No | gpt-5.2 | Model (e.g., gpt-5.2) |
121125
| `API_AGENT_PORT` | No | 3000 | Server port |
126+
| `API_AGENT_ENABLE_RECIPES` | No | true | Enable recipe learning & caching |
127+
| `API_AGENT_RECIPE_CACHE_SIZE` | No | 64 | Max cached recipes (LRU eviction) |
122128
| `OTEL_EXPORTER_OTLP_ENDPOINT` | No | - | OpenTelemetry tracing endpoint |
123129

124130
---
@@ -181,6 +187,47 @@ flowchart TB
181187

182188
---
183189

190+
## Recipe Learning
191+
192+
Agent automatically learns reusable patterns from successful queries. When you ask a question, the agent:
193+
194+
1. **Executes** - Runs API calls + SQL post-processing via LLM reasoning
195+
2. **Extracts** - LLM converts execution trace into parameterized template
196+
3. **Caches** - Stores recipe keyed by (API, schema hash) with fuzzy question matching
197+
4. **Reuses** - Similar future questions execute cached recipe instantly (no LLM reasoning)
198+
199+
```mermaid
200+
flowchart LR
201+
subgraph First["First Query"]
202+
Q1["'Top 5 users by age'"]
203+
A1["Agent reasons"]
204+
E1["API + SQL"]
205+
R1["Recipe extracted"]
206+
end
207+
208+
subgraph Cache["Recipe Cache"]
209+
T["Template:<br/>LIMIT {{limit}}"]
210+
end
211+
212+
subgraph Reuse["Similar Query"]
213+
Q2["'Top 10 users by age'"]
214+
M["Fuzzy match"]
215+
X["Execute directly"]
216+
end
217+
218+
Q1 --> A1 --> E1 --> R1 --> T
219+
Q2 --> M --> T --> X
220+
```
221+
222+
**Recipe structure:**
223+
- **GraphQL**: `query_template` with `{{param}}` placeholders
224+
- **REST**: `path_params`, `query_params`, `body` with `{"$param": "name"}` refs
225+
- **SQL**: `{{param}}` in SQL strings
226+
227+
Recipes auto-expire when schema changes (hash mismatch). Disable with `API_AGENT_ENABLE_RECIPES=false`.
228+
229+
---
230+
184231
## Development
185232

186233
```bash
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Utilities for safe ContextVar access."""
2+
3+
from contextvars import ContextVar
4+
from typing import TypeVar
5+
6+
T = TypeVar("T")
7+
8+
9+
def safe_get_contextvar(var: ContextVar[T], default: T) -> T:
10+
"""Safe ContextVar access with default.
11+
12+
Returns value if set, otherwise returns default without setting.
13+
"""
14+
try:
15+
return var.get()
16+
except LookupError:
17+
return default
18+
19+
20+
def safe_append_contextvar_list(var: ContextVar[list[T]], item: T) -> None:
21+
"""Append to ContextVar list if initialized.
22+
23+
Silently ignores if ContextVar not set (expected in some contexts).
24+
"""
25+
try:
26+
var.get().append(item)
27+
except LookupError:
28+
pass # Expected - list not initialized for this context

0 commit comments

Comments
 (0)