Skip to content

Commit 169600a

Browse files
authored
Merge pull request #8 from agoda-com/feat/openapi-2.0-support
feat: add Swagger 2.0 support and structured HTTP error details
2 parents 1cc779e + 35ebe47 commit 169600a

File tree

13 files changed

+696
-21
lines changed

13 files changed

+696
-21
lines changed

CLAUDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ docker run -p 3000:3000 -e OPENAI_API_KEY="..." api-agent
8585

8686
- **api_agent/utils/**: Shared utilities
8787
- **csv.py**: CSV conversion via DuckDB (for recipe `return_directly` output)
88+
- **http_errors.py**: HTTP error response extraction (used by both clients)
8889

8990
- **api_agent/graphql/**: GraphQL client (httpx)
90-
- **api_agent/rest/**: REST client (httpx) + OpenAPI loader
91+
- **api_agent/rest/**: REST client (httpx) + OpenAPI loader (supports OpenAPI 3.x and Swagger 2.0)
9192
- **api_agent/executor.py**: DuckDB SQL execution, table extraction, context truncation
9293

9394
### Context Management
@@ -131,6 +132,16 @@ Query → Agent executes → Extractor LLM → Recipe stored → MCP tool `r_{na
131132
- **Templating**: GraphQL `{{param}}`, REST `{"$param": "name"}`, SQL `{{param}}`
132133
- **Config**: `ENABLE_RECIPES` (default: True), `RECIPE_CACHE_SIZE` (default: 64)
133134

135+
## After Code Changes
136+
137+
Always run before marking task complete:
138+
```bash
139+
uv run ruff check --fix api_agent/ # Lint + auto-fix
140+
uv run ruff format api_agent/ # Format
141+
uv run ty check # Type check
142+
uv run pytest tests/ -v # Tests
143+
```
144+
134145
## Testing Notes
135146

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

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ That's it. Agent introspects schema, generates queries, runs SQL post-processing
5656

5757
## More Examples
5858

59-
**REST API (Petstore):**
59+
**REST API (Petstore — OpenAPI 3.x):**
6060
```json
6161
{
6262
"mcpServers": {
@@ -71,6 +71,21 @@ That's it. Agent introspects schema, generates queries, runs SQL post-processing
7171
}
7272
```
7373

74+
**REST API (Petstore — Swagger 2.0):**
75+
```json
76+
{
77+
"mcpServers": {
78+
"petstore": {
79+
"url": "http://localhost:3000/mcp",
80+
"headers": {
81+
"X-Target-URL": "https://petstore.swagger.io/v2/swagger.json",
82+
"X-API-Type": "rest"
83+
}
84+
}
85+
}
86+
}
87+
```
88+
7489
**Your own API with auth:**
7590
```json
7691
{
@@ -95,7 +110,7 @@ That's it. Agent introspects schema, generates queries, runs SQL post-processing
95110

96111
| Header | Required | Description |
97112
| ---------------------- | -------- | ---------------------------------------------------------- |
98-
| `X-Target-URL` | Yes | GraphQL endpoint OR OpenAPI spec URL |
113+
| `X-Target-URL` | Yes | GraphQL endpoint OR OpenAPI/Swagger spec URL (3.x and 2.0) |
99114
| `X-API-Type` | Yes | `graphql` or `rest` |
100115
| `X-Target-Headers` | No | JSON auth headers, e.g. `{"Authorization": "Bearer xxx"}` |
101116
| `X-API-Name` | No | Override tool name prefix (default: auto-generated) |

api_agent/agent/graphql_agent.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,11 @@ async def graphql_query(query: str, name: str = "data", return_directly: bool =
391391
indent=2,
392392
)
393393

394+
if not result.get("success"):
395+
result["hint"] = (
396+
"Use search_schema to find valid field names, enum values, or required args"
397+
)
398+
394399
return json.dumps(result, indent=2)
395400

396401
return graphql_query
@@ -463,7 +468,10 @@ def _create_individual_recipe_tools(
463468
tool_name = deduplicate_tool_name(s.get("tool_name", "unknown_recipe"), seen_names)
464469
params_spec = recipe.get("params", {})
465470
docstring = build_recipe_docstring(
466-
s["question"], recipe.get("steps", []), recipe.get("sql_steps", []), "graphql",
471+
s["question"],
472+
recipe.get("steps", []),
473+
recipe.get("sql_steps", []),
474+
"graphql",
467475
params_spec=params_spec,
468476
)
469477

api_agent/agent/rest_agent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,9 @@ def _create_individual_recipe_tools(
538538
tool_name = deduplicate_tool_name(s.get("tool_name", "unknown_recipe"), seen_names)
539539
params_spec = recipe.get("params", {})
540540
docstring = build_recipe_docstring(
541-
s["question"], recipe.get("steps", []), recipe.get("sql_steps", []),
541+
s["question"],
542+
recipe.get("steps", []),
543+
recipe.get("sql_steps", []),
542544
params_spec=params_spec,
543545
)
544546

api_agent/graphql/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import httpx
88

9+
from ..utils.http_errors import build_http_error_response
10+
911
logger = logging.getLogger(__name__)
1012

1113
# Block mutations (read-only mode)
@@ -57,7 +59,7 @@ async def execute_query(
5759
return {"success": False, "error": result["errors"]}
5860
return {"success": True, "data": result.get("data", {})}
5961
except httpx.HTTPStatusError as e:
60-
return {"success": False, "error": f"HTTP {e.response.status_code}"}
62+
return build_http_error_response(e)
6163
except Exception as e:
6264
logger.exception("GraphQL error")
6365
return {"success": False, "error": str(e)}

api_agent/recipe/common.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,9 @@ def build_recipe_docstring(
202202
if params_spec:
203203
param_lines = []
204204
for pname, spec in params_spec.items():
205-
ptype = _JSON_TYPE_NAMES.get(spec.get("type", "str") if isinstance(spec, dict) else "str", "string")
205+
ptype = _JSON_TYPE_NAMES.get(
206+
spec.get("type", "str") if isinstance(spec, dict) else "str", "string"
207+
)
206208
example = spec.get("default") if isinstance(spec, dict) else None
207209
hint = f" (e.g. {example})" if example is not None else ""
208210
param_lines.append(f" {pname}: {ptype} REQUIRED{hint}")

api_agent/rest/client.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import httpx
99

10+
from ..utils.http_errors import build_http_error_response
11+
1012
logger = logging.getLogger(__name__)
1113

1214
# Unsafe HTTP methods (blocked by default)
@@ -138,15 +140,7 @@ async def execute_request(
138140
return {"success": True, "data": data}
139141

140142
except httpx.HTTPStatusError as e:
141-
# Try to get error body
142-
try:
143-
error_body = e.response.json()
144-
except Exception:
145-
error_body = e.response.text[:500]
146-
return {
147-
"success": False,
148-
"error": f"HTTP {e.response.status_code}: {error_body}",
149-
}
143+
return build_http_error_response(e)
150144
except Exception as e:
151145
logger.exception("REST API error")
152146
return {"success": False, "error": str(e)}

0 commit comments

Comments
 (0)