fastmcp-builder 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp_builder-0.3.0.dist-info/METADATA +125 -0
- fastmcp_builder-0.3.0.dist-info/RECORD +50 -0
- fastmcp_builder-0.3.0.dist-info/WHEEL +4 -0
- fastmcp_builder-0.3.0.dist-info/entry_points.txt +4 -0
- mcpforge/__init__.py +23 -0
- mcpforge/api_client.py +158 -0
- mcpforge/cli.py +906 -0
- mcpforge/discovery.py +64 -0
- mcpforge/doctor.py +101 -0
- mcpforge/generator.py +64 -0
- mcpforge/generator_ts.py +36 -0
- mcpforge/inspection.py +145 -0
- mcpforge/mcp_server.py +247 -0
- mcpforge/models.py +189 -0
- mcpforge/openapi.py +243 -0
- mcpforge/planner.py +58 -0
- mcpforge/prompts/__init__.py +23 -0
- mcpforge/prompts/generator.md +180 -0
- mcpforge/prompts/generator_multi.md +59 -0
- mcpforge/prompts/generator_ts.md +49 -0
- mcpforge/prompts/planner.md +152 -0
- mcpforge/prompts/self_heal.md +24 -0
- mcpforge/prompts/test_gen.md +82 -0
- mcpforge/prompts/test_gen_ts.md +44 -0
- mcpforge/prompts/updater.md +15 -0
- mcpforge/providers.py +112 -0
- mcpforge/py.typed +0 -0
- mcpforge/sandbox.py +69 -0
- mcpforge/security.py +160 -0
- mcpforge/self_heal.py +204 -0
- mcpforge/template_hints.py +50 -0
- mcpforge/templates/README.md.j2 +139 -0
- mcpforge/templates/config.json.j2 +13 -0
- mcpforge/templates/env.example.j2 +3 -0
- mcpforge/templates/fastmcp.json.j2 +27 -0
- mcpforge/templates/init_server.py.j2 +15 -0
- mcpforge/templates/init_test.py.j2 +11 -0
- mcpforge/templates/pyproject.toml.j2 +18 -0
- mcpforge/templates/ts/README.md.j2 +44 -0
- mcpforge/templates/ts/config.json.j2 +8 -0
- mcpforge/templates/ts/env.example.j2 +3 -0
- mcpforge/templates/ts/gitignore.j2 +4 -0
- mcpforge/templates/ts/package.json.j2 +23 -0
- mcpforge/templates/ts/tsconfig.json.j2 +15 -0
- mcpforge/test_generator.py +27 -0
- mcpforge/updater.py +56 -0
- mcpforge/utils.py +11 -0
- mcpforge/validator.py +310 -0
- mcpforge/validator_ts.py +139 -0
- mcpforge/writer.py +206 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fastmcp-builder
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Generate complete, runnable FastMCP 3.x MCP servers from plain-English descriptions
|
|
5
|
+
Keywords: mcp,fastmcp,anthropic,claude,code-generation,llm
|
|
6
|
+
Author: saagpatel
|
|
7
|
+
Author-email: saagpatel <saagarpatel08@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Dist: anthropic>=0.87.0
|
|
17
|
+
Requires-Dist: click>=8.1.0
|
|
18
|
+
Requires-Dist: fastmcp>=3.2.3
|
|
19
|
+
Requires-Dist: jinja2>=3.1.6
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Requires-Dist: rich>=15.0.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Homepage, https://github.com/saagpatel/mcpforge
|
|
25
|
+
Project-URL: Repository, https://github.com/saagpatel/mcpforge
|
|
26
|
+
Project-URL: Bug Tracker, https://github.com/saagpatel/mcpforge/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# mcpforge
|
|
30
|
+
|
|
31
|
+
[](#) [](#)
|
|
32
|
+
|
|
33
|
+
> One sentence. One command. A complete MCP server, ready to run.
|
|
34
|
+
|
|
35
|
+
mcpforge generates production-ready FastMCP 3.x MCP servers from plain-English descriptions. You describe what you want; it produces tools, input validation, error handling, a pytest test suite, run configuration, and client setup docs — all wired together and ready to inspect, validate, and install.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Plain-English generation** — describe your server in natural language; Claude writes the implementation
|
|
40
|
+
- **Complete project scaffold** — tools, Pydantic input models, error handling, `pyproject.toml`, and a pytest suite generated together
|
|
41
|
+
- **FastMCP 3.x native** — output uses modern FastMCP decorators and transport configuration, not raw MCP protocol boilerplate
|
|
42
|
+
- **Validate before running** — `mcpforge validate` runs syntax, security, lint, import, and pytest checks against generated servers
|
|
43
|
+
- **Iterate safely** — `mcpforge update` modifies an existing generated server and backs up changed files before writing
|
|
44
|
+
- **Discover generated servers** — `mcpforge list` finds mcpforge-generated projects in a workspace
|
|
45
|
+
- **Inspect and diagnose** — `mcpforge inspect` summarizes generated server shape, while `mcpforge doctor` checks local readiness
|
|
46
|
+
- **Machine-readable output** — status-like commands expose `--json` for agent workflows
|
|
47
|
+
- **OpenAPI curation controls** — include/exclude tags, operation allowlists, and operation limits keep generated integrations focused
|
|
48
|
+
- **Scaffold without an LLM** — `mcpforge init` creates a minimal FastMCP server skeleton for local iteration
|
|
49
|
+
- **MCP server mode** — `mcpforge-server` exposes generation, planning, validation, inspection, doctor, and discovery tools so AI assistants can build safely
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Prerequisites
|
|
54
|
+
- Python 3.12+
|
|
55
|
+
- `uv` (recommended)
|
|
56
|
+
- Anthropic API key
|
|
57
|
+
|
|
58
|
+
### Installation
|
|
59
|
+
```bash
|
|
60
|
+
uv tool install fastmcp-builder
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The PyPI distribution is `fastmcp-builder`; the installed commands remain
|
|
64
|
+
`mcpforge` and `mcpforge-server`.
|
|
65
|
+
|
|
66
|
+
### Usage
|
|
67
|
+
```bash
|
|
68
|
+
# Generate a new MCP server
|
|
69
|
+
mcpforge generate "A todo list manager with create, read, update, and delete operations"
|
|
70
|
+
|
|
71
|
+
# Validate an existing generated server
|
|
72
|
+
mcpforge validate ./my-server
|
|
73
|
+
|
|
74
|
+
# Modify an existing generated server
|
|
75
|
+
mcpforge update ./my-server "Add a tool to export todos as CSV"
|
|
76
|
+
|
|
77
|
+
# Find generated servers in the current workspace
|
|
78
|
+
mcpforge list . --recursive
|
|
79
|
+
|
|
80
|
+
# Inspect a generated server without executing it
|
|
81
|
+
mcpforge inspect ./my-server
|
|
82
|
+
|
|
83
|
+
# Check local prerequisites and provider readiness
|
|
84
|
+
mcpforge doctor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Useful generation flags:
|
|
88
|
+
- `--dry-run` displays the structured plan without writing files.
|
|
89
|
+
- `--no-execute` writes files but skips import and test execution.
|
|
90
|
+
- `--strict` treats lint errors as hard validation failures.
|
|
91
|
+
- `--from-openapi FILE` generates from an OpenAPI 3.x spec.
|
|
92
|
+
- `--openapi-include-tag TAG`, `--openapi-exclude-tag TAG`, `--openapi-operation ID`, and `--openapi-limit N` curate OpenAPI conversion.
|
|
93
|
+
- `--language python|typescript` chooses the target server language.
|
|
94
|
+
- `--provider anthropic|openai` selects the generation provider. OpenAI is listed as planned and remains gated until deterministic structured-output smokes are implemented.
|
|
95
|
+
|
|
96
|
+
Useful status flags:
|
|
97
|
+
- `mcpforge list --json`
|
|
98
|
+
- `mcpforge inspect PATH --json`
|
|
99
|
+
- `mcpforge validate PATH --json`
|
|
100
|
+
- `mcpforge doctor --json`
|
|
101
|
+
- `mcpforge version --json`
|
|
102
|
+
|
|
103
|
+
## Tech Stack
|
|
104
|
+
|
|
105
|
+
| Layer | Technology |
|
|
106
|
+
|-------|------------|
|
|
107
|
+
| Language | Python 3.12+ |
|
|
108
|
+
| Generation | Anthropic Claude via `anthropic` SDK |
|
|
109
|
+
| MCP framework | FastMCP 3.x |
|
|
110
|
+
| CLI | Click 8 |
|
|
111
|
+
| Templates | Jinja2 |
|
|
112
|
+
| Validation | Pydantic v2 |
|
|
113
|
+
| Output | Rich |
|
|
114
|
+
|
|
115
|
+
## Architecture
|
|
116
|
+
|
|
117
|
+
The `generate` command sends the user's description to Claude with a structured prompt that includes FastMCP 3.x idioms and a tool-schema contract. Claude returns a JSON plan (tool names, signatures, and descriptions) that mcpforge validates against a Pydantic model before rendering through Jinja2 templates into a complete project directory. The generated project is then validated with syntax checks, security scanning, ruff linting, import checks, and pytest execution. The `update` command reads an existing generated server, asks Claude for a targeted modification, writes backups for changed files, and validates the result.
|
|
118
|
+
|
|
119
|
+
## Current Status
|
|
120
|
+
|
|
121
|
+
As of May 10, 2026, `main` is prepared as `0.3.0` for the `fastmcp-builder` PyPI distribution. The v0.3 builder lane expands mcpforge from runnable-server generation into a production integration builder with inspection, doctor checks, richer generated scaffolds, OpenAPI curation, MCP server parity, provider abstraction, and live generated fixture examples for REST API, filesystem, database, and TypeScript profiles. See `docs/CURRENT-STATE.md` and `docs/ROADMAP-v0.3.md`.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
mcpforge/__init__.py,sha256=wT-IwBzwKTWou8-EZp9HilBLiKvVihTkl_v_j5EhDY8,449
|
|
2
|
+
mcpforge/api_client.py,sha256=-7fpLjexuchgYQPJXNv5xND_qQ1jAFRkUJQBZ2THuCY,5860
|
|
3
|
+
mcpforge/cli.py,sha256=l9mKY1t35QYOpZQrJqXvBrstbw1G9fW0fldL_g8yBAo,32868
|
|
4
|
+
mcpforge/discovery.py,sha256=zP21zm4RSXxx0At1_QYVQ2y-ebTUID7GybqWrqdGWc8,2386
|
|
5
|
+
mcpforge/doctor.py,sha256=FLNcbsKwhkHHxr4UJf-wQWMUIVeeIhl3crizQHX5-u0,3379
|
|
6
|
+
mcpforge/generator.py,sha256=c4AFaw4DpMJpNofH6wPl_4zuXZpOGEUw8aRTg08myKw,2141
|
|
7
|
+
mcpforge/generator_ts.py,sha256=IXYyru0K_r6lobbQB2X6t-4zqXG0cd1w4mP--OqeZr4,1206
|
|
8
|
+
mcpforge/inspection.py,sha256=YJZZnkqK8dTG0WFKfkemmbKQS31ZKippVK_Nbo7FFCg,4867
|
|
9
|
+
mcpforge/mcp_server.py,sha256=FQclaxKmSihqCCb5A-OwvCdOVX8XvoMcuMI4gT_5owQ,9159
|
|
10
|
+
mcpforge/models.py,sha256=i6X49xwYs7Z8oTAes3mUcGBROVPbdWkgIAQXESkz73k,5650
|
|
11
|
+
mcpforge/openapi.py,sha256=l0PMWScd0FLdWSx6_-fAME-MSizxgwIHNxgr0Ue6YQo,8781
|
|
12
|
+
mcpforge/planner.py,sha256=nPGYIIf3XuSmso2lKUNvA336Lt2txfC4K9_oMKTBnac,1995
|
|
13
|
+
mcpforge/prompts/__init__.py,sha256=ECWc4caHwgAondA320i2LBY9xjH-nxe1ClCwMZK0t3Y,716
|
|
14
|
+
mcpforge/prompts/generator.md,sha256=Oy8Kl12HGZn0dX7cMaxb2dEkcB0JNKm76xRJ4Jp0jn0,6658
|
|
15
|
+
mcpforge/prompts/generator_multi.md,sha256=TLg0eDWUrvi5749oDmMHzvFa7ncBdmcYU7_aJDwFJIY,2170
|
|
16
|
+
mcpforge/prompts/generator_ts.md,sha256=h80ppzWNNHfnlkTJ58bhZUMYQfkqSgqN5GZkr2c0Pzk,2070
|
|
17
|
+
mcpforge/prompts/planner.md,sha256=01qkPCHN1Ki1lzaIoM3yn9ybwk6N9W9DFrxbg5-jK_E,6020
|
|
18
|
+
mcpforge/prompts/self_heal.md,sha256=91eV8RAinqoXvx7hGhd_QmgYXUn7Ok6nVLCTgEToTUg,943
|
|
19
|
+
mcpforge/prompts/test_gen.md,sha256=116Ww6DrNXFNy4i1KPH-DojjHYXKWufmC4kg2_yk1F8,2941
|
|
20
|
+
mcpforge/prompts/test_gen_ts.md,sha256=vY5OVasQNj8g_XgdHFycEC3aBVL4HJizwpxFr5n1sE0,1886
|
|
21
|
+
mcpforge/prompts/updater.md,sha256=8WSq6H-mcdqdflKFoIyHdYSVZoACNfz6lYNupaU7yb0,753
|
|
22
|
+
mcpforge/providers.py,sha256=xYoiC7jiIAKLYmDixcpDtnoVqf6gj7jZJbmS9NNOK94,3178
|
|
23
|
+
mcpforge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
mcpforge/sandbox.py,sha256=0x38jOtfATwxSdgQfLyIfpmWdoj1BWTssXPZgXbrxZs,2218
|
|
25
|
+
mcpforge/security.py,sha256=iDhyfdKOG8krOqERwYfbD2u_HqZhMg_Z5nWI0Uq-lGQ,4496
|
|
26
|
+
mcpforge/self_heal.py,sha256=C9ceTOBHF9nMGH_dYD_fhWrN2ioy_BP6OnqIkKNZdLk,7026
|
|
27
|
+
mcpforge/template_hints.py,sha256=0KaKL54d1b1ES4lN8kUIU42sjAq2xHOod0Bfoo1C1CI,3028
|
|
28
|
+
mcpforge/templates/README.md.j2,sha256=MTSNaGEK4wo2SyImTHaqoSXFtMLaWJVw-Ip8B5STYGs,2374
|
|
29
|
+
mcpforge/templates/config.json.j2,sha256=Ex-xDWMyuzmuRGu07r6zyX-SY5SYl0apIpHsE456yGs,287
|
|
30
|
+
mcpforge/templates/env.example.j2,sha256=lXUCyQwvi7Xltg27xgp1sAshMqTUEzB4stKwrKpjSSw,57
|
|
31
|
+
mcpforge/templates/fastmcp.json.j2,sha256=tPLJTuj4lil3vNpTBaUyJ-7tk1lQun5UnptCu8pqn9k,742
|
|
32
|
+
mcpforge/templates/init_server.py.j2,sha256=tlb10xY_EEu-tUzC9WWYMOWZr3H33x4W7TthKO_YyEI,316
|
|
33
|
+
mcpforge/templates/init_test.py.j2,sha256=YtJLC6SSUWNfPpCivL3jdyUON-sRCl87U3-BORRfLLk,266
|
|
34
|
+
mcpforge/templates/pyproject.toml.j2,sha256=KeD3OIIzEcczoMPEgcdPx9UrGEGbXsMvY2Bg3sfD4Zg,381
|
|
35
|
+
mcpforge/templates/ts/README.md.j2,sha256=4J4TKB_4h59_YgTkNWMLRvjBNzj1_04YDRLZ6fXBKvA,548
|
|
36
|
+
mcpforge/templates/ts/config.json.j2,sha256=7QIFtMEPQtSRlVATETEqaemydxVjCPZr64wqHWyaN0c,129
|
|
37
|
+
mcpforge/templates/ts/env.example.j2,sha256=lXUCyQwvi7Xltg27xgp1sAshMqTUEzB4stKwrKpjSSw,57
|
|
38
|
+
mcpforge/templates/ts/gitignore.j2,sha256=TWmWbLSs_Gv5On9EEDDrJRLuZuneiM_J7354ZLWGiPY,34
|
|
39
|
+
mcpforge/templates/ts/package.json.j2,sha256=oTYakFCjI_OOoYLoEvAvMw7YQ5GffbYbwUwPBRKhYO8,503
|
|
40
|
+
mcpforge/templates/ts/tsconfig.json.j2,sha256=bPNP99SGoUiKCUr7YWitD3istXcXdvUOLAAZYAE-3ls,326
|
|
41
|
+
mcpforge/test_generator.py,sha256=o-ZKTw77W3MpCfq--mnq_oQfo0uXftHFHCJjJc1eBNk,869
|
|
42
|
+
mcpforge/updater.py,sha256=Y5njvl5i-2ymYsA6E9HLCKWyXiqP27iubwfLEX2XREE,1853
|
|
43
|
+
mcpforge/utils.py,sha256=DjMxtGMbzEfkKpYzU6eMuSu6F918wSC6lFsujAeIBBo,290
|
|
44
|
+
mcpforge/validator.py,sha256=QawzWqLD4spXIpzfPItoOwwP9Pn8igj5PGFSzrYDFnE,11111
|
|
45
|
+
mcpforge/validator_ts.py,sha256=KWxI-laizuL2fGxlxaHwTYGhop8SRuWF1gJSVGAw69M,4524
|
|
46
|
+
mcpforge/writer.py,sha256=7zuDibuP6cfDp6YGLl6UmeO7T3KOz9S63mMSpt0WA3E,7286
|
|
47
|
+
fastmcp_builder-0.3.0.dist-info/WHEEL,sha256=-unyBNsdNo6Y_XVO25zjvxasG_uUFN_fBd_kLyKE-Fk,81
|
|
48
|
+
fastmcp_builder-0.3.0.dist-info/entry_points.txt,sha256=BtXe9KuwIOsuC9QT_mwUsc-PIxQzCo1MNT-_4Inqm_w,93
|
|
49
|
+
fastmcp_builder-0.3.0.dist-info/METADATA,sha256=Y8rLZ3w24pe4qkbCeqBAKzODJEUmHjsvoZ5EJCTj25s,6109
|
|
50
|
+
fastmcp_builder-0.3.0.dist-info/RECORD,,
|
mcpforge/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""mcpforge — Generate FastMCP 3.x MCP servers from plain-English descriptions."""
|
|
2
|
+
|
|
3
|
+
from mcpforge.api_client import DEFAULT_MODEL
|
|
4
|
+
from mcpforge.models import (
|
|
5
|
+
PromptDef,
|
|
6
|
+
ResourceDef,
|
|
7
|
+
ServerPlan,
|
|
8
|
+
ToolDef,
|
|
9
|
+
ToolParam,
|
|
10
|
+
ValidationResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.3.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PromptDef",
|
|
16
|
+
"ResourceDef",
|
|
17
|
+
"ServerPlan",
|
|
18
|
+
"ToolDef",
|
|
19
|
+
"ToolParam",
|
|
20
|
+
"ValidationResult",
|
|
21
|
+
"DEFAULT_MODEL",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
mcpforge/api_client.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Anthropic API client wrapper with retry logic.
|
|
2
|
+
|
|
3
|
+
MODEL PIN: This module uses Sonnet 4.6 intentionally. Opus 4.7 rejects
|
|
4
|
+
non-default `temperature` values (400 error), and mcpforge relies on
|
|
5
|
+
`temperature=0` for deterministic JSON output in `generate_json()`.
|
|
6
|
+
Do NOT upgrade the default model to Opus 4.7 (or any future model that
|
|
7
|
+
rejects temperature) without first migrating `generate_json()` to tool
|
|
8
|
+
use with a strict schema, which is the 4.7-era replacement for the
|
|
9
|
+
`temperature=0` determinism lever.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import random
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
import anthropic
|
|
20
|
+
from pydantic import BaseModel, ValidationError
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODEL = "claude-sonnet-4-6"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnthropicClient:
|
|
26
|
+
"""Async wrapper around anthropic.AsyncAnthropic with exponential-backoff retry."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
model: str = DEFAULT_MODEL,
|
|
32
|
+
) -> None:
|
|
33
|
+
resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
|
34
|
+
if not resolved_key:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Anthropic API key is required. "
|
|
37
|
+
"Set the ANTHROPIC_API_KEY environment variable or pass api_key= explicitly."
|
|
38
|
+
)
|
|
39
|
+
self._model = model
|
|
40
|
+
self._client = anthropic.AsyncAnthropic(api_key=resolved_key)
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"AnthropicClient(model={self._model!r})"
|
|
44
|
+
|
|
45
|
+
async def generate(
|
|
46
|
+
self,
|
|
47
|
+
system_prompt: str,
|
|
48
|
+
user_message: str,
|
|
49
|
+
max_tokens: int = 8192,
|
|
50
|
+
temperature: float = 0.0,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Send a message and return the text response.
|
|
53
|
+
|
|
54
|
+
Retries up to 3 times with exponential backoff on transient provider errors.
|
|
55
|
+
"""
|
|
56
|
+
last_exc: Exception | None = None
|
|
57
|
+
for attempt in range(3):
|
|
58
|
+
try:
|
|
59
|
+
response = await self._client.messages.create(
|
|
60
|
+
model=self._model,
|
|
61
|
+
max_tokens=max_tokens,
|
|
62
|
+
temperature=temperature,
|
|
63
|
+
system=system_prompt,
|
|
64
|
+
messages=[{"role": "user", "content": user_message}],
|
|
65
|
+
)
|
|
66
|
+
return response.content[0].text # type: ignore[union-attr]
|
|
67
|
+
except anthropic.RateLimitError as exc:
|
|
68
|
+
last_exc = exc
|
|
69
|
+
if attempt == 2:
|
|
70
|
+
raise
|
|
71
|
+
wait = (2**attempt) + random.uniform(0.0, 1.0)
|
|
72
|
+
await asyncio.sleep(wait)
|
|
73
|
+
except anthropic.APIConnectionError as exc:
|
|
74
|
+
last_exc = exc
|
|
75
|
+
if attempt == 2:
|
|
76
|
+
raise
|
|
77
|
+
wait = (2**attempt) + random.uniform(0.0, 1.0)
|
|
78
|
+
await asyncio.sleep(wait)
|
|
79
|
+
except anthropic.APIStatusError as exc:
|
|
80
|
+
last_exc = exc
|
|
81
|
+
if exc.status_code >= 500 and attempt < 2:
|
|
82
|
+
wait = (2**attempt) + random.uniform(0.0, 1.0)
|
|
83
|
+
await asyncio.sleep(wait)
|
|
84
|
+
continue
|
|
85
|
+
raise
|
|
86
|
+
raise RuntimeError("generate() exited retry loop unexpectedly") from last_exc
|
|
87
|
+
|
|
88
|
+
async def generate_json(
|
|
89
|
+
self,
|
|
90
|
+
system_prompt: str,
|
|
91
|
+
user_message: str,
|
|
92
|
+
response_model: type[BaseModel],
|
|
93
|
+
max_tokens: int = 8192,
|
|
94
|
+
) -> BaseModel:
|
|
95
|
+
"""Generate and parse a structured JSON response into a Pydantic model instance.
|
|
96
|
+
|
|
97
|
+
Uses temperature=0 for deterministic JSON output. Strips markdown fences
|
|
98
|
+
before parsing so the LLM can optionally wrap its response in ```json blocks.
|
|
99
|
+
"""
|
|
100
|
+
text = await self.generate(
|
|
101
|
+
system_prompt=system_prompt,
|
|
102
|
+
user_message=user_message,
|
|
103
|
+
max_tokens=max_tokens,
|
|
104
|
+
temperature=0.0,
|
|
105
|
+
)
|
|
106
|
+
try:
|
|
107
|
+
data = _extract_json(text)
|
|
108
|
+
return response_model.model_validate(data)
|
|
109
|
+
except json.JSONDecodeError as exc:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Response was not valid JSON for {response_model.__name__}.\n"
|
|
112
|
+
f"Raw response (first 500 chars): {text[:500]}"
|
|
113
|
+
) from exc
|
|
114
|
+
except ValidationError as exc:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"Response JSON did not match {response_model.__name__} schema: {exc}"
|
|
117
|
+
) from exc
|
|
118
|
+
|
|
119
|
+
async def generate_stream(
|
|
120
|
+
self,
|
|
121
|
+
system_prompt: str,
|
|
122
|
+
user_message: str,
|
|
123
|
+
max_tokens: int = 16384,
|
|
124
|
+
temperature: float = 0.2,
|
|
125
|
+
chunk_timeout: float = 60.0,
|
|
126
|
+
) -> AsyncIterator[str]:
|
|
127
|
+
"""Stream text chunks as they arrive from the API.
|
|
128
|
+
|
|
129
|
+
Yields str chunks. No retry — streaming connections are not retryable.
|
|
130
|
+
Raises TimeoutError if no chunk arrives within chunk_timeout seconds.
|
|
131
|
+
"""
|
|
132
|
+
async with self._client.messages.stream(
|
|
133
|
+
model=self._model,
|
|
134
|
+
max_tokens=max_tokens,
|
|
135
|
+
temperature=temperature,
|
|
136
|
+
system=system_prompt,
|
|
137
|
+
messages=[{"role": "user", "content": user_message}],
|
|
138
|
+
) as stream:
|
|
139
|
+
aiter = stream.text_stream.__aiter__()
|
|
140
|
+
while True:
|
|
141
|
+
try:
|
|
142
|
+
text = await asyncio.wait_for(aiter.__anext__(), timeout=chunk_timeout)
|
|
143
|
+
yield text
|
|
144
|
+
except StopAsyncIteration:
|
|
145
|
+
break
|
|
146
|
+
except TimeoutError:
|
|
147
|
+
raise TimeoutError(
|
|
148
|
+
f"Streaming generation stalled — no data received for {chunk_timeout}s"
|
|
149
|
+
) from None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_json(text: str) -> dict:
|
|
153
|
+
"""Extract a JSON object from text, stripping optional markdown code fences."""
|
|
154
|
+
text = text.strip()
|
|
155
|
+
if text.startswith("```"):
|
|
156
|
+
text = re.sub(r"^```(?:json)?\s*\n?", "", text)
|
|
157
|
+
text = re.sub(r"\n?```\s*$", "", text)
|
|
158
|
+
return json.loads(text.strip())
|