diff --git a/.gitignore b/.gitignore index ceddaa3..46e8bab 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .cache/ +site/ diff --git a/docs/_generated/README.md b/docs/_generated/README.md new file mode 100644 index 0000000..ad9ab2e --- /dev/null +++ b/docs/_generated/README.md @@ -0,0 +1,4 @@ +This directory contains generated Markdown includes used by the docs. + +Do not edit files here directly — run `python generate_reference_docs.py` from the repo root to regenerate. + diff --git a/docs/_generated/_generation_warnings.md b/docs/_generated/_generation_warnings.md new file mode 100644 index 0000000..61d5c53 --- /dev/null +++ b/docs/_generated/_generation_warnings.md @@ -0,0 +1 @@ +Generated alias tables successfully, but skipped import-based references: `ModuleNotFoundError: No module named 'mcp'` diff --git a/docs/_generated/model_aliases_anthropic.md b/docs/_generated/model_aliases_anthropic.md new file mode 100644 index 0000000..07892ad --- /dev/null +++ b/docs/_generated/model_aliases_anthropic.md @@ -0,0 +1,9 @@ +| Model Alias | Maps to | Model Alias | Maps to | +| --- | --- | --- | --- | +| `claude` | `claude-sonnet-4-5` | `opus4` | `claude-opus-4-1` | +| `haiku` | `claude-haiku-4-5` | `opus45` | `claude-opus-4-5` | +| `haiku3` | `claude-3-haiku-20240307` | `sonnet` | `claude-sonnet-4-5` | +| `haiku35` | `claude-3-5-haiku-latest` | `sonnet35` | `claude-3-5-sonnet-latest` | +| `haiku45` | `claude-haiku-4-5` | `sonnet37` | `claude-3-7-sonnet-latest` | +| `opus` | `claude-opus-4-5` | `sonnet4` | `claude-sonnet-4-0` | +| `opus3` | `claude-3-opus-latest` | `sonnet45` | `claude-sonnet-4-5` | diff --git a/docs/_generated/model_aliases_deepseek.md b/docs/_generated/model_aliases_deepseek.md new file mode 100644 index 0000000..4f987ad --- /dev/null +++ b/docs/_generated/model_aliases_deepseek.md @@ -0,0 +1,5 @@ +| Model Alias | Maps to | +| --- | --- | +| `deepseek` | `deepseek-chat` | +| `deepseek3` | `deepseek-chat` | +| `deepseekv3` | `deepseek-chat` | diff --git a/docs/_generated/model_aliases_google.md b/docs/_generated/model_aliases_google.md new file mode 100644 index 0000000..f622bc8 --- /dev/null +++ b/docs/_generated/model_aliases_google.md @@ -0,0 +1,6 @@ +| Model Alias | Maps to | +| --- | --- | +| `gemini2` | `gemini-2.0-flash` | +| `gemini25` | `gemini-2.5-flash-preview-09-2025` | +| `gemini25pro` | `gemini-2.5-pro` | +| `gemini3` | `gemini-3-pro-preview` | diff --git a/docs/_generated/model_aliases_groq.md b/docs/_generated/model_aliases_groq.md new file mode 100644 index 0000000..d563e29 --- /dev/null +++ b/docs/_generated/model_aliases_groq.md @@ -0,0 +1,3 @@ +| Model Alias | Maps to | +| --- | --- | +| `kimigroq` | `groq.moonshotai/kimi-k2-instruct-0905` | diff --git a/docs/_generated/model_aliases_openai.md b/docs/_generated/model_aliases_openai.md new file mode 100644 index 0000000..d3128c2 --- /dev/null +++ b/docs/_generated/model_aliases_openai.md @@ -0,0 +1,11 @@ +| Model Alias | Maps to | Model Alias | Maps to | +| --- | --- | --- | --- | +| `gpt-4.1` | `gpt-4.1` | `gpt-5.1-mini` | `gpt-5.1-mini` | +| `gpt-4.1-mini` | `gpt-4.1-mini` | `gpt-5.1-nano` | `gpt-5.1-nano` | +| `gpt-4.1-nano` | `gpt-4.1-nano` | `gpt51` | `openai.gpt-5.1` | +| `gpt-4o` | `gpt-4o` | `o1` | `o1` | +| `gpt-4o-mini` | `gpt-4o-mini` | `o1-mini` | `o1-mini` | +| `gpt-5` | `gpt-5` | `o1-preview` | `o1-preview` | +| `gpt-5-mini` | `gpt-5-mini` | `o3` | `o3` | +| `gpt-5-nano` | `gpt-5-nano` | `o3-mini` | `o3-mini` | +| `gpt-5.1` | `gpt-5.1` | `o4-mini` | `o4-mini` | diff --git a/docs/_generated/model_aliases_xai.md b/docs/_generated/model_aliases_xai.md new file mode 100644 index 0000000..8a7b213 --- /dev/null +++ b/docs/_generated/model_aliases_xai.md @@ -0,0 +1,10 @@ +| Model Alias | Maps to | +| --- | --- | +| `grok-3` | `grok-3` | +| `grok-3-fast` | `grok-3-fast` | +| `grok-3-mini` | `grok-3-mini` | +| `grok-3-mini-fast` | `grok-3-mini-fast` | +| `grok-4` | `grok-4` | +| `grok-4-0709` | `grok-4-0709` | +| `grok-4-fast` | `xai.grok-4-fast-non-reasoning` | +| `grok-4-fast-reasoning` | `xai.grok-4-fast-reasoning` | diff --git a/docs/agents/defining.md b/docs/agents/defining.md index 5ef57c2..92c44da 100644 --- a/docs/agents/defining.md +++ b/docs/agents/defining.md @@ -1,4 +1,4 @@ -# Defining Agents and Workflows +# Defining Agents ## Basic Agents @@ -60,85 +60,9 @@ from pathlib import Path ``` -## Workflows and MCP Servers +See [Workflows](workflows.md) for chaining, routing, parallelism, orchestrators, and MAKER. -_To generate examples use `fast-agent quickstart workflow`. This example can be run with `uv run workflow/chaining.py`. fast-agent looks for configuration files in the current directory before checking parent directories recursively._ - -Agents can be chained to build a workflow, using MCP Servers defined in the `fastagent.config.yaml` file: - -```yaml title="fastagent.config.yaml" -# Example of a STDIO sever named "fetch" -mcp: - servers: - fetch: - command: "uvx" - args: ["mcp-server-fetch"] -``` - - -```python title="social.py" -@fast.agent( - "url_fetcher", - "Given a URL, provide a complete and comprehensive summary", - servers=["fetch"], # Name of an MCP Server defined in fastagent.config.yaml -) -@fast.agent( - "social_media", - """ - Write a 280 character social media post for any given text. - Respond only with the post, never use hashtags. - """, -) -@fast.chain( - name="post_writer", - sequence=["url_fetcher", "social_media"], -) -async def main(): - async with fast.run() as agent: - # using chain workflow - await agent.post_writer("http://fast-agent.ai") -``` - -All Agents and Workflows respond to `.send("message")`. The agent app responds to `.interactive()` to start a chat session. - -Saved as `social.py` we can now run this workflow from the command line with: - -```bash -uv run workflow/chaining.py --agent post_writer --message "" -``` - -Add the `--quiet` switch to disable progress and message display and return only the final response - useful for simple automations. - -Read more about running **fast-agent** agents [here](running.md) - -## Workflow Types - -**fast-agent** has built-in support for the patterns referenced in Anthropic's [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents) paper. - -### Chain - -The `chain` workflow offers a declarative approach to calling Agents in sequence: - -```python - -@fast.chain( - "post_writer", - sequence=["url_fetcher","social_media"] -) - -# we can them prompt it directly: -async with fast.run() as agent: - await agent.interactive(agent="post_writer") - -``` - -This starts an interactive session, which produces a short social media post for a given URL. If a _chain_ is prompted it returns to a chat with last Agent in the chain. You can switch agents by typing `@agent-name`. - -Chains can be incorporated in other workflows, or contain other workflow elements (including other Chains). You can set an `instruction` to describe it's capabilities to other workflow steps if needed. - -Chains are also helpful for capturing content before being dispatched by a `router`, or summarizing content before being used in the downstream workflow. - -### Human Input +## Human Input Agents can request Human Input to assist with a task or get additional context: @@ -151,84 +75,7 @@ Agents can request Human Input to assist with a task or get additional context: await agent("print the next number in the sequence") ``` -In the example `human_input.py`, the Agent will prompt the User for additional information to complete the task. - -### Parallel - -The Parallel Workflow sends the same message to multiple Agents simultaneously (`fan-out`), then uses the `fan-in` Agent to process the combined content. - -```python -@fast.agent("translate_fr", "Translate the text to French") -@fast.agent("translate_de", "Translate the text to German") -@fast.agent("translate_es", "Translate the text to Spanish") - -@fast.parallel( - name="translate", - fan_out=["translate_fr","translate_de","translate_es"] -) - -@fast.chain( - "post_writer", - sequence=["url_fetcher","social_media","translate"] -) -``` - -If you don't specify a `fan-in` agent, the `parallel` returns the combined Agent results verbatim. - -`parallel` is also useful to ensemble ideas from different LLMs. - -When using `parallel` in other workflows, specify an `instruction` to describe its operation. - -### Evaluator-Optimizer - -Evaluator-Optimizers combine 2 agents: one to generate content (the `generator`), and the other to judge that content and provide actionable feedback (the `evaluator`). Messages are sent to the generator first, then the pair run in a loop until either the evaluator is satisfied with the quality, or the maximum number of refinements is reached. The final result from the Generator is returned. - -If the Generator has `use_history` off, the previous iteration is returned when asking for improvements - otherwise conversational context is used. - -```python -@fast.evaluator_optimizer( - name="researcher", - generator="web_searcher", - evaluator="quality_assurance", - min_rating="EXCELLENT", - max_refinements=3 -) - -async with fast.run() as agent: - await agent.researcher.send("produce a report on how to make the perfect espresso") -``` - -When used in a workflow, it returns the last `generator` message as the result. - -See the `evaluator.py` workflow example, or `fast-agent quickstart researcher` for a more complete example. - -### Router - -Routers use an LLM to assess a message, and route it to the most appropriate Agent. The routing prompt is automatically generated based on the Agent instructions and available Servers. - -```python -@fast.router( - name="route", - agents=["agent1","agent2","agent3"] -) -``` - -NB - If only one agent is supplied to the router, it forwards directly. - -Look at the `router.py` workflow for an example. - -### Orchestrator - -Given a complex task, the Orchestrator uses an LLM to generate a plan to divide the task amongst the available Agents. The planning and aggregation prompts are generated by the Orchestrator, which benefits from using more capable models. Plans can either be built once at the beginning (`plantype="full"`) or iteratively (`plantype="iterative"`). - -```python -@fast.orchestrator( - name="orchestrate", - agents=["task1","task2","task3"] -) -``` - -See the `orchestrator.py` or `agent_build.py` workflow example. +In the example `human_input.py`, the agent will prompt the user for additional information to complete the task. ## Agent and Workflow Reference @@ -257,7 +104,7 @@ You can customize how an agent interacts with the LLM by passing `request_params ### Example ```python -from fast_agent.core.request_params import RequestParams +from fast_agent.types import RequestParams @fast.agent( name="CustomAgent", # name of the agent @@ -306,85 +153,16 @@ from fast_agent.core.request_params import RequestParams ) ``` -#### Chain - -```python -@fast.chain( - name="chain", # name of the chain - sequence=["agent1", "agent2", ...], # list of agents in execution order - instruction="instruction", # instruction to describe the chain for other workflows - cumulative=False, # whether to accumulate messages through the chain - continue_with_final=True, # open chat with agent at end of chain after prompting -) -``` - -#### Parallel - -```python -@fast.parallel( - name="parallel", # name of the parallel workflow - fan_out=["agent1", "agent2"], # list of agents to run in parallel - fan_in="aggregator", # name of agent that combines results (optional) - instruction="instruction", # instruction to describe the parallel for other workflows - include_request=True, # include original request in fan-in message -) -``` - -#### Evaluator-Optimizer - -```python -@fast.evaluator_optimizer( - name="researcher", # name of the workflow - generator="web_searcher", # name of the content generator agent - evaluator="quality_assurance", # name of the evaluator agent - min_rating="GOOD", # minimum acceptable quality (EXCELLENT, GOOD, FAIR, POOR) - max_refinements=3, # maximum number of refinement iterations -) -``` - -#### Router - -```python -@fast.router( - name="route", # name of the router - agents=["agent1", "agent2", "agent3"], # list of agent names router can delegate to - instruction="routing instruction", # any extra routing instructions - servers=["filesystem"], # list of servers for the routing agent - #tools={"filesystem": ["tool_1", "tool_2"] # Filter the tools available to the agent. Defaults to all - #resources={"filesystem: ["resource_1", "resource_2"]} # Filter the resources available to the agent. Defaults to all - #prompts={"filesystem": ["prompt_1", "prompt_2"]} # Filter the prompts available to the agent. Defaults to all - model="o3-mini.high", # specify routing model - use_history=False, # router maintains conversation history - human_input=False, # whether router can request human input - api_key="programmatic-api-key", # specify the API KEY programmatically, it will override which provided in config file or env var -) -``` - -#### Orchestrator - -```python -@fast.orchestrator( - name="orchestrator", # name of the orchestrator - instruction="instruction", # base instruction for the orchestrator - agents=["agent1", "agent2"], # list of agent names this orchestrator can use - model="o3-mini.high", # specify orchestrator planning model - use_history=False, # orchestrator doesn't maintain chat history (no effect). - human_input=False, # whether orchestrator can request human input - plan_type="full", # planning approach: "full" or "iterative" - max_iterations=5, # maximum number of full plan attempts, or iterations - api_key="programmatic-api-key", # specify the API KEY programmatically, it will override which provided in config file or env var -) -``` +Workflow definitions (chain/parallel/router/orchestrator/maker) are documented on the [Workflows](workflows.md) page. #### Custom ```python @fast.custom( - cls=Custom # agent class + cls=Custom, # agent class name="custom", # name of the custom agent instruction="instruction", # base instruction for the orchestrator servers=["filesystem"], # list of MCP Servers for the agent - MCP Servers for the agent #tools={"filesystem": ["tool_1", "tool_2"] # Filter the tools available to the agent. Defaults to all #resources={"filesystem: ["resource_1", "resource_2"]} # Filter the resources available to the agent. Defaults to all #prompts={"filesystem": ["prompt_1", "prompt_2"]} # Filter the prompts available to the agent. Defaults to all @@ -396,4 +174,3 @@ from fast_agent.core.request_params import RequestParams api_key="programmatic-api-key", # specify the API KEY programmatically, it will override which provided in config file or env var ) ``` - diff --git a/docs/agents/workflows.md b/docs/agents/workflows.md new file mode 100644 index 0000000..ff6b12b --- /dev/null +++ b/docs/agents/workflows.md @@ -0,0 +1,260 @@ +# Workflows + +Workflows let you compose multiple agents into a single higher-level capability (e.g. chaining steps, routing, or adding reliability via voting). They can be used alongside MCP servers defined in `fastagent.config.yaml`. + +## Workflows and MCP Servers + +To generate examples use `fast-agent quickstart workflow`. + +Agents can use MCP Servers defined in `fastagent.config.yaml`: + +```yaml title="fastagent.config.yaml" +# Example of a STDIO sever named "fetch" +mcp: + servers: + fetch: + command: "uvx" + args: ["mcp-server-fetch"] +``` + +```python title="social.py" +@fast.agent( + "url_fetcher", + "Given a URL, provide a complete and comprehensive summary", + servers=["fetch"], # Name of an MCP Server defined in fastagent.config.yaml +) +@fast.agent( + "social_media", + """ + Write a 280 character social media post for any given text. + Respond only with the post, never use hashtags. + """, +) +@fast.chain( + name="post_writer", + sequence=["url_fetcher", "social_media"], +) +async def main(): + async with fast.run() as agent: + await agent.post_writer.send("http://fast-agent.ai") +``` + +Saved as `social.py` you can run the workflow from the command line with: + +```bash +uv run social.py --agent post_writer --message "" +``` + +Add the `--quiet` switch to disable progress and message display and return only the final response. + +Read more about running **fast-agent** agents [here](running.md) + +## Workflow Types + +**fast-agent** has built-in support for common agentic workflow patterns (including those referenced in Anthropic's [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents)). + +### Chain + +The `chain` workflow offers a declarative approach to calling Agents in sequence. + +```python +@fast.chain( + name="post_writer", + sequence=["url_fetcher", "social_media"], +) + +async with fast.run() as agent: + await agent.interactive(agent="post_writer") +``` + +Chains can be incorporated in other workflows, or contain other workflow elements (including other Chains). You can set an `instruction` to describe its capabilities to other workflow steps if needed. + +### Parallel + +The `parallel` workflow sends the same message to multiple agents simultaneously (`fan_out`), then optionally uses a `fan_in` agent to process the combined content. + +```python +@fast.agent("translate_fr", "Translate the text to French") +@fast.agent("translate_de", "Translate the text to German") +@fast.agent("translate_es", "Translate the text to Spanish") + +@fast.parallel( + name="translate", + fan_out=["translate_fr", "translate_de", "translate_es"], +) +``` + +If you don't specify a `fan_in` agent, `parallel` returns the combined agent results verbatim. + +### Evaluator-Optimizer + +Evaluator-Optimizers combine 2 agents: one to generate content (the `generator`), and the other to judge that content and provide actionable feedback (the `evaluator`). Messages are sent to the generator first, then the pair run in a loop until either the evaluator is satisfied with the quality, or the maximum number of refinements is reached. The final result from the generator is returned. + +```python +@fast.evaluator_optimizer( + name="researcher", + generator="web_searcher", + evaluator="quality_assurance", + min_rating="EXCELLENT", + max_refinements=3, +) + +async with fast.run() as agent: + await agent.researcher.send("produce a report on how to make the perfect espresso") +``` + +### Router + +Routers use an LLM to assess a message and route it to the most appropriate agent. The routing prompt is automatically generated based on the agent instructions and available servers. + +```python +@fast.router( + name="route", + agents=["agent1", "agent2", "agent3"], +) +``` + +NB - If only one agent is supplied to the router, it forwards directly. + +### Orchestrator + +Given a complex task, the Orchestrator uses an LLM to generate a plan to divide the task amongst the available Agents. Plans can either be built once at the beginning (`plan_type="full"`) or iteratively (`plan_type="iterative"`). + +```python +@fast.orchestrator( + name="orchestrate", + agents=["task1", "task2", "task3"], +) +``` + +### Iterative Planner + +The `iterative_planner` workflow is a specialized orchestrator for long-running plans that are refined over multiple iterations. + +```python +@fast.iterative_planner( + name="planner", + agents=["task1", "task2", "task3"], +) +``` + +### MAKER + +MAKER (“Massively decomposed Agentic processes with K-voting Error Reduction”) wraps a worker agent and samples it repeatedly until a response achieves a k-vote margin over all alternatives (“first-to-ahead-by-k” voting). This is useful for long chains of simple steps where rare errors would otherwise compound. + +- Reference: [Solving a Million-Step LLM Task with Zero Errors](https://arxiv.org/abs/2511.09030) +- Credit: Lucid Programmer (PR author) + +```python +@fast.agent( + name="classifier", + instruction="Reply with only: A, B, or C.", +) +@fast.maker( + name="reliable_classifier", + worker="classifier", + k=3, + max_samples=25, + match_strategy="normalized", + red_flag_max_length=16, +) +async def main(): + async with fast.run() as agent: + await agent.reliable_classifier.send("Classify: ...") +``` + +## Workflow Reference + +### Chain + +```python +@fast.chain( + name="chain", + sequence=["agent1", "agent2", ...], + instruction="instruction", + cumulative=False, +) +``` + +### Parallel + +```python +@fast.parallel( + name="parallel", + fan_out=["agent1", "agent2"], + fan_in="aggregator", + instruction="instruction", + include_request=True, +) +``` + +### Evaluator-Optimizer + +```python +@fast.evaluator_optimizer( + name="researcher", + generator="web_searcher", + evaluator="quality_assurance", + instruction="instruction", + min_rating="GOOD", + max_refinements=3, + refinement_instruction="optional guidance", +) +``` + +### Router + +```python +@fast.router( + name="route", + agents=["agent1", "agent2", "agent3"], + instruction="routing instruction", + servers=["filesystem"], + model="o3-mini.high", + use_history=False, + human_input=False, + api_key="programmatic-api-key", +) +``` + +### Orchestrator + +```python +@fast.orchestrator( + name="orchestrator", + instruction="instruction", + agents=["agent1", "agent2"], + model="o3-mini.high", + use_history=False, + human_input=False, + plan_type="full", + plan_iterations=5, + api_key="programmatic-api-key", +) +``` + +### Iterative Planner + +```python +@fast.iterative_planner( + name="planner", + agents=["agent1", "agent2"], + model="o3-mini.high", + plan_iterations=-1, + api_key="programmatic-api-key", +) +``` + +### MAKER + +```python +@fast.maker( + name="maker", + worker="worker_agent", + k=3, + max_samples=50, + match_strategy="exact", # exact|normalized|structured + red_flag_max_length=256, + instruction="instruction", +) +``` diff --git a/docs/models/llm_providers.md b/docs/models/llm_providers.md index f1f2214..10f879a 100644 --- a/docs/models/llm_providers.md +++ b/docs/models/llm_providers.md @@ -38,13 +38,7 @@ anthropic: **Model Name Aliases:** -| Model Alias | Maps to | Model Alias | Maps to | -| ----------- | -------------------------- | ----------- | -------------------------- | -| `claude` | `claude-sonnet-4-0` | `haiku` | `claude-3-5-haiku-latest` | -| `sonnet` | `claude-sonnet-4-0` | `haiku3` | `claude-3-haiku-20240307` | -| `sonnet35` | `claude-3-5-sonnet-latest` | `haiku35` | `claude-3-5-haiku-latest` | -| `sonnet37` | `claude-3-7-sonnet-latest` | `opus` | `claude-opus-4-1` | -| `opus3` | `claude-3-opus-latest` | | | +--8<-- "_generated/model_aliases_anthropic.md" ## OpenAI @@ -76,15 +70,7 @@ openai: **Model Name Aliases:** -| Model Alias | Maps to | Model Alias | Maps to | -| ------------- | ------------- | ------------- | ------------- | -| `gpt-4o` | `gpt-4o` | `gpt-4.1` | `gpt-4.1` | -| `gpt-4o-mini` | `gpt-4o-mini` | `gpt-4.1-mini`| `gpt-4.1-mini`| -| `o1` | `o1` | `gpt-4.1-nano`| `gpt-4.1-nano`| -| `o1-mini` | `o1-mini` | `o1-preview` | `o1-preview` | -| `o3-mini` | `o3-mini` | `o3` | | -| `gpt-5` | `gpt-5` | `gpt-5-mini` | `gpt-5-mini` | -| `gpt-5-nano` | `gpt-5-nano` | | | +--8<-- "_generated/model_aliases_openai.md" @@ -264,9 +250,7 @@ groq: **Model Name Aliases:** -| Model Alias | Maps to | -| ----------- | -------------------------- | -| `kimigroq` | `moonshotai/kimi-k2-instruct` | +--8<-- "_generated/model_aliases_groq.md" ## DeepSeek @@ -288,10 +272,7 @@ deepseek: **Model Name Aliases:** -| Model Alias | Maps to | -| ----------- | -------------------------- | -| `deepseek` | `deepseek-chat` | -| `deepseek3` | `deepseek-chat` | +--8<-- "_generated/model_aliases_deepseek.md" ## Google @@ -312,11 +293,7 @@ google: **Model Name Aliases:** -| Model Alias | Maps to | -| ----------- | -------------------------- | -| `gemini2` | `gemini-2.0-flash` | -| `gemini25` | `gemini-2.5-flash-preview-05-20` | -| `gemini25pro` | `gemini-2.5-pro-preview-05-06` | +--8<-- "_generated/model_aliases_google.md" ### OpenAI Mode @@ -341,15 +318,7 @@ xai: **Model Name Aliases:** -| Model Alias | Maps to (xai.) | -| ----------- | -------------------------- | -| `grok-3` | `grok-3` | -| `grok-3-fast` | `grok-3-fast` | -| `grok-3-mini` | `grok-3-mini` | -| `grok-3-mini-fast` | `grok-3-mini-fast` | -| `grok-4` | `grok-4` | -| `grok-4-fast` | `grok-4-fast-non-reasoning` | -| `grok-4-fast-reasoning` | `grok-4-fast-reasoning` | +--8<-- "_generated/model_aliases_xai.md" ## Generic OpenAI / Ollama diff --git a/docs/ref/generated_docs.md b/docs/ref/generated_docs.md new file mode 100644 index 0000000..ca09a4b --- /dev/null +++ b/docs/ref/generated_docs.md @@ -0,0 +1,20 @@ +# Generated Docs + +Some parts of the documentation are generated from the `fast-agent` Python package to prevent drift (e.g. model alias tables). + +## Regenerate + +From the `fast-agent-docs` repo root: + +```bash +python generate_reference_docs.py +``` + +If you don't have `fast_agent` installed in the docs venv, run it against a local checkout: + +```bash +FAST_AGENT_REPO_PATH=../fast-agent python generate_reference_docs.py +``` + +Generated files are written to `docs/_generated/` and included in pages via MkDocs `pymdownx.snippets`. + diff --git a/generate_reference_docs.py b/generate_reference_docs.py new file mode 100644 index 0000000..c9af4e5 --- /dev/null +++ b/generate_reference_docs.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import ast +import inspect +import os +import sys +from pathlib import Path +from typing import Any + + +DOCS_ROOT = Path(__file__).resolve().parent +GENERATED_DIR = DOCS_ROOT / "docs" / "_generated" + + +def _find_fast_agent_repo() -> Path: + """ + Locate a local fast-agent repo checkout. + + Uses FAST_AGENT_REPO_PATH when set, otherwise assumes ../fast-agent next to this repo. + """ + repo_override = os.getenv("FAST_AGENT_REPO_PATH") + candidate = Path(repo_override).resolve() if repo_override else (DOCS_ROOT.parent / "fast-agent") + candidate = candidate.resolve() + + expected = candidate / "src" / "fast_agent" / "llm" / "model_factory.py" + if expected.exists(): + return candidate + + raise SystemExit( + "Could not locate fast-agent source.\n" + "Set FAST_AGENT_REPO_PATH to the fast-agent repo root (the directory containing `src/fast_agent`)." + ) + + +def _try_enable_fast_agent_import(repo_root: Path) -> None: + """ + Best-effort enable imports from a local `fast-agent` checkout. + + Some generated references (e.g. RequestParams field docs) require importing fast_agent, + which may fail if the docs environment doesn't have runtime deps installed. + """ + src_root = repo_root / "src" + if src_root.exists(): + sys.path.insert(0, str(src_root)) + sys.path.insert(0, str(repo_root)) + + +def _write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def _md_code(lang: str, code: str) -> str: + return f"```{lang}\n{code.rstrip()}\n```\n" + + +def _format_signature(name: str, func: Any) -> str: + sig = str(inspect.signature(func)) + return f"{name}{sig}" + + +def generate_workflows_reference() -> str: + from fast_agent.core.fastagent import FastAgent + + fast = FastAgent("docs-reference") + + workflows: list[tuple[str, Any]] = [ + ("chain", fast.chain), + ("parallel", fast.parallel), + ("evaluator_optimizer", fast.evaluator_optimizer), + ("router", fast.router), + ("orchestrator", fast.orchestrator), + ("iterative_planner", fast.iterative_planner), + ("maker", fast.maker), + ] + + lines: list[str] = [] + lines.append("\n\n") + lines.append("## Workflow Decorators (Generated)\n\n") + lines.append( + "These signatures are generated from the installed `fast_agent` package to prevent drift.\n\n" + ) + + for name, method in workflows: + lines.append(f"### `{name}`\n\n") + lines.append(_md_code("python", _format_signature(f"fast.{name}", method))) + + return "".join(lines) + + +def generate_request_params_reference() -> str: + from fast_agent.types import RequestParams + + lines: list[str] = [] + lines.append("\n\n") + lines.append("### Available `RequestParams` Fields (Generated)\n\n") + lines.append("| Field | Type | Default | Description |\n") + lines.append("| --- | --- | --- | --- |\n") + + for field_name, field_info in RequestParams.model_fields.items(): + annotation = field_info.annotation + type_str = getattr(annotation, "__name__", None) or str(annotation) + default = field_info.default + if default is None and field_info.default_factory is not None: + default_str = "``" + else: + default_str = "`None`" if default is None else f"`{default!r}`" + + desc = (field_info.description or "").replace("\n", " ").strip() + lines.append(f"| `{field_name}` | `{type_str}` | {default_str} | {desc} |\n") + + return "".join(lines) + + +def _format_alias_table(entries: list[tuple[str, str]], *, two_column: bool) -> str: + def fmt_cell(s: str) -> str: + return f"`{s}`" if s else "" + + entries = sorted(entries, key=lambda t: t[0].lower()) + + if not entries: + return "_No aliases defined._\n" + + if two_column: + lines: list[str] = [] + lines.append("| Model Alias | Maps to |\n") + lines.append("| --- | --- |\n") + for alias, target in entries: + lines.append(f"| {fmt_cell(alias)} | {fmt_cell(target)} |\n") + return "".join(lines) + + # 4-column layout (two alias columns side-by-side) + half = (len(entries) + 1) // 2 + left = entries[:half] + right = entries[half:] + + lines = [] + lines.append("| Model Alias | Maps to | Model Alias | Maps to |\n") + lines.append("| --- | --- | --- | --- |\n") + for i in range(half): + a1, t1 = left[i] + if i < len(right): + a2, t2 = right[i] + else: + a2, t2 = "", "" + lines.append(f"| {fmt_cell(a1)} | {fmt_cell(t1)} | {fmt_cell(a2)} | {fmt_cell(t2)} |\n") + return "".join(lines) + + +def _provider_name_map(repo_root: Path) -> dict[str, str]: + """ + Map Provider enum member name -> provider config string (e.g. OPENAI -> "openai"). + """ + provider_types = repo_root / "src" / "fast_agent" / "llm" / "provider_types.py" + tree = ast.parse(provider_types.read_text(encoding="utf-8")) + + mapping: dict[str, str] = {} + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "Provider": + for stmt in node.body: + if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1 and isinstance( + stmt.targets[0], ast.Name + ): + key = stmt.targets[0].id + # Provider members are assigned tuples like ("openai", "OpenAI") + if isinstance(stmt.value, ast.Tuple) and stmt.value.elts: + first = stmt.value.elts[0] + if isinstance(first, ast.Constant) and isinstance(first.value, str): + mapping[key] = first.value + return mapping + + +def _load_model_factory_constants( + repo_root: Path, +) -> tuple[dict[str, str], dict[str, str], set[str], set[str]]: + """ + Load ModelFactory.MODEL_ALIASES and ModelFactory.DEFAULT_PROVIDERS from source using AST. + Returns (model_aliases, default_providers, effort_suffixes). + """ + model_factory = repo_root / "src" / "fast_agent" / "llm" / "model_factory.py" + tree = ast.parse(model_factory.read_text(encoding="utf-8")) + + provider_map = _provider_name_map(repo_root) + provider_names: set[str] = set(provider_map.values()) + + model_aliases: dict[str, str] = {} + default_providers: dict[str, str] = {} + effort_suffixes: set[str] = set() + + for node in tree.body: + if isinstance(node, ast.ClassDef) and node.name == "ModelFactory": + for stmt in node.body: + if not isinstance(stmt, ast.Assign) or len(stmt.targets) != 1: + continue + if not isinstance(stmt.targets[0], ast.Name): + continue + target_name = stmt.targets[0].id + + if target_name == "MODEL_ALIASES" and isinstance(stmt.value, ast.Dict): + for k, v in zip(stmt.value.keys, stmt.value.values): + if ( + isinstance(k, ast.Constant) + and isinstance(k.value, str) + and isinstance(v, ast.Constant) + and isinstance(v.value, str) + ): + model_aliases[k.value] = v.value + + if target_name == "DEFAULT_PROVIDERS" and isinstance(stmt.value, ast.Dict): + for k, v in zip(stmt.value.keys, stmt.value.values): + if not (isinstance(k, ast.Constant) and isinstance(k.value, str)): + continue + # Values are Provider.OPENAI etc + if ( + isinstance(v, ast.Attribute) + and isinstance(v.value, ast.Name) + and v.value.id == "Provider" + ): + provider_member = v.attr + provider_name = provider_map.get(provider_member) + if provider_name: + default_providers[k.value] = provider_name + + if target_name == "EFFORT_MAP" and isinstance(stmt.value, ast.Dict): + for k in stmt.value.keys: + if isinstance(k, ast.Constant) and isinstance(k.value, str): + effort_suffixes.add(k.value.lower()) + + return model_aliases, default_providers, effort_suffixes, provider_names + + +def _infer_provider_for_model_string( + model_string: str, *, default_providers: dict[str, str], provider_names: set[str], effort_suffixes: set[str] +) -> str | None: + """ + Infer provider from a model string using the same high-level rules as ModelFactory.parse_model_string. + """ + base = model_string.rsplit(":", 1)[0] + parts = base.split(".") + + # Strip reasoning suffix if present + if len(parts) > 1 and parts[-1].lower() in effort_suffixes: + base = ".".join(parts[:-1]) + parts = base.split(".") + + if parts and parts[0] in provider_names: + return parts[0] + + return default_providers.get(base) + + +def _include_default_model_name(provider_name: str, model_name: str) -> bool: + """ + Heuristic for which "default provider" model names to show in provider docs. + + Goal: keep tables readable by excluding heavily versioned names. + """ + # Exclude date/version stamped releases in the alias tables + if "-20" in model_name: + return False + # Exclude long Bedrock ids etc (not shown via this table) + if provider_name == "bedrock": + return False + return True + + +def generate_model_alias_table( + provider_name: str, + *, + include_default_models: bool, + two_column: bool = True, + repo_root: Path, +) -> str: + """ + Generate a provider-specific model alias table from fast-agent source-of-truth. + + Includes: + - "default provider" model names (e.g. `gpt-5` defaults to OpenAI) + - short aliases from ModelFactory.MODEL_ALIASES (e.g. `sonnet` -> `claude-sonnet-4-5`) + """ + model_aliases, default_providers, effort_suffixes, provider_names = _load_model_factory_constants( + repo_root + ) + + entries: dict[str, str] = {} + + if include_default_models: + for model_name, default_provider in default_providers.items(): + if default_provider == provider_name and _include_default_model_name(provider_name, model_name): + entries[model_name] = model_name + + for alias, target in model_aliases.items(): + inferred = _infer_provider_for_model_string( + target, + default_providers=default_providers, + provider_names=provider_names, + effort_suffixes=effort_suffixes, + ) + if inferred == provider_name: + entries[alias] = target + + return _format_alias_table(list(entries.items()), two_column=two_column) + + +def main() -> int: + GENERATED_DIR.mkdir(parents=True, exist_ok=True) + repo_root = _find_fast_agent_repo() + + # Alias tables are generated from source (AST) so they work even when fast_agent runtime deps + # aren't installed in the docs environment. + _write( + GENERATED_DIR / "model_aliases_anthropic.md", + generate_model_alias_table( + "anthropic", + include_default_models=False, + two_column=False, + repo_root=repo_root, + ), + ) + _write( + GENERATED_DIR / "model_aliases_openai.md", + generate_model_alias_table( + "openai", + include_default_models=True, + two_column=False, + repo_root=repo_root, + ), + ) + _write( + GENERATED_DIR / "model_aliases_groq.md", + generate_model_alias_table( + "groq", + include_default_models=False, + two_column=True, + repo_root=repo_root, + ), + ) + _write( + GENERATED_DIR / "model_aliases_deepseek.md", + generate_model_alias_table( + "deepseek", + include_default_models=False, + two_column=True, + repo_root=repo_root, + ), + ) + _write( + GENERATED_DIR / "model_aliases_google.md", + generate_model_alias_table( + "google", + include_default_models=False, + two_column=True, + repo_root=repo_root, + ), + ) + _write( + GENERATED_DIR / "model_aliases_xai.md", + generate_model_alias_table( + "xai", + include_default_models=True, + two_column=True, + repo_root=repo_root, + ), + ) + + # Best-effort: these require importing `fast_agent` (and its runtime deps). + _try_enable_fast_agent_import(repo_root) + try: + _write(GENERATED_DIR / "workflows_reference.md", generate_workflows_reference()) + _write(GENERATED_DIR / "request_params_reference.md", generate_request_params_reference()) + except Exception as exc: + _write( + GENERATED_DIR / "_generation_warnings.md", + f"Generated alias tables successfully, but skipped import-based references: `{type(exc).__name__}: {exc}`\n", + ) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/mkdocs.yml b/mkdocs.yml index 7c6c3fa..f755508 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,7 +45,9 @@ markdown_extensions: - attr_list - admonition - pymdownx.inlinehilite - - pymdownx.snippets + - pymdownx.snippets: + base_path: docs + check_paths: true - pymdownx.superfences - md_in_html - pymdownx.highlight: @@ -87,6 +89,7 @@ nav: # - Installation: getting_started/installation.md - Agents: - agents/defining.md + - Workflows: agents/workflows.md - agents/running.md - agents/prompting.md - System Prompts: agents/instructions.md @@ -113,6 +116,7 @@ nav: - fast-agent go: ref/go_command.md - Config File: ref/config_file.md - Command Line: ref/cmd_switches.md + - Generated Docs: ref/generated_docs.md - Class Reference: ref/class_reference.md - Open Telemetry: ref/open_telemetry.md - Azure Configuration: ref/azure-config.md