Fixes issues with OpenRouter/Anthropic (disabling streaming until better Pydantic AI implementation)
This commit is contained in:
parent
53cfd5e6a7
commit
fcca718d3a
@ -2,16 +2,18 @@
|
||||
# OpenAI: https://api.openai.com/v1
|
||||
# Ollama (example): http://localhost:11434/v1
|
||||
# OpenRouter: https://openrouter.ai/api/v1
|
||||
# Anthropic: https://api.anthropic.com/v1
|
||||
BASE_URL=
|
||||
|
||||
# For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
|
||||
# For Anthropic: https://console.anthropic.com/account/keys
|
||||
# For OpenRouter: https://openrouter.ai/keys
|
||||
# For Ollama, no need to set this unless you specifically configured an API key
|
||||
LLM_API_KEY=
|
||||
|
||||
# Get your Open AI API Key by following these instructions -
|
||||
# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
|
||||
# Even if using OpenRouter, you still need to set this for the embedding model.
|
||||
# Even if using Anthropic or OpenRouter, you still need to set this for the embedding model.
|
||||
# No need to set this if using Ollama.
|
||||
OPENAI_API_KEY=
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ Since V4 is the current version of Archon, all the code for V4 is in both the ma
|
||||
- Docker (optional but preferred)
|
||||
- Python 3.11+
|
||||
- Supabase account (for vector database)
|
||||
- OpenAI/OpenRouter API key or Ollama for local LLMs
|
||||
- OpenAI/Anthropic/OpenRouter API key or Ollama for local LLMs (note that only OpenAI supports streaming in the Streamlit UI currently)
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from pydantic_ai.models.anthropic import AnthropicModel
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
@ -31,21 +32,29 @@ logfire.configure(send_to_logfire='never')
|
||||
|
||||
base_url = get_env_var('BASE_URL') or 'https://api.openai.com/v1'
|
||||
api_key = get_env_var('LLM_API_KEY') or 'no-llm-api-key-provided'
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
reasoner_llm_model = get_env_var('REASONER_MODEL') or 'o3-mini'
|
||||
is_anthropic = "anthropic" in base_url.lower()
|
||||
is_openai = "openai" in base_url.lower()
|
||||
|
||||
reasoner_llm_model_name = get_env_var('REASONER_MODEL') or 'o3-mini'
|
||||
reasoner_llm_model = AnthropicModel(reasoner_llm_model_name, api_key=api_key) if is_anthropic else OpenAIModel(reasoner_llm_model_name, base_url=base_url, api_key=api_key)
|
||||
|
||||
reasoner = Agent(
|
||||
OpenAIModel(reasoner_llm_model, base_url=base_url, api_key=api_key),
|
||||
reasoner_llm_model,
|
||||
system_prompt='You are an expert at coding AI agents with Pydantic AI and defining the scope for doing so.',
|
||||
)
|
||||
|
||||
primary_llm_model = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
primary_llm_model_name = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
primary_llm_model = AnthropicModel(primary_llm_model_name, api_key=api_key) if is_anthropic else OpenAIModel(primary_llm_model_name, base_url=base_url, api_key=api_key)
|
||||
|
||||
router_agent = Agent(
|
||||
OpenAIModel(primary_llm_model, base_url=base_url, api_key=api_key),
|
||||
primary_llm_model,
|
||||
system_prompt='Your job is to route the user message either to the end of the conversation or to continue coding the AI agent.',
|
||||
)
|
||||
|
||||
end_conversation_agent = Agent(
|
||||
OpenAIModel(primary_llm_model, base_url=base_url, api_key=api_key),
|
||||
primary_llm_model,
|
||||
system_prompt='Your job is to end a conversation for creating an AI agent by giving instructions for how to execute the agent and they saying a nice goodbye to the user.',
|
||||
)
|
||||
|
||||
@ -124,7 +133,7 @@ async def coder_agent(state: AgentState, writer):
|
||||
message_history.extend(ModelMessagesTypeAdapter.validate_json(message_row))
|
||||
|
||||
# Run the agent in a stream
|
||||
if is_ollama:
|
||||
if not is_openai:
|
||||
writer = get_stream_writer()
|
||||
result = await pydantic_ai_coder.run(state['latest_user_message'], deps=deps, message_history= message_history)
|
||||
writer(result.data)
|
||||
@ -178,7 +187,7 @@ async def finish_conversation(state: AgentState, writer):
|
||||
message_history.extend(ModelMessagesTypeAdapter.validate_json(message_row))
|
||||
|
||||
# Run the agent in a stream
|
||||
if is_ollama:
|
||||
if not is_openai:
|
||||
writer = get_stream_writer()
|
||||
result = await end_conversation_agent.run(state['latest_user_message'], message_history= message_history)
|
||||
writer(result.data)
|
||||
|
||||
@ -11,6 +11,7 @@ import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pydantic import BaseModel
|
||||
from pydantic_ai import Agent, ModelRetry, RunContext
|
||||
from pydantic_ai.models.anthropic import AnthropicModel
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from openai import AsyncOpenAI
|
||||
from supabase import Client
|
||||
@ -24,13 +25,15 @@ load_dotenv()
|
||||
llm = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
base_url = get_env_var('BASE_URL') or 'https://api.openai.com/v1'
|
||||
api_key = get_env_var('LLM_API_KEY') or 'no-llm-api-key-provided'
|
||||
model = OpenAIModel(llm, base_url=base_url, api_key=api_key)
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
is_anthropic = "anthropic" in base_url.lower()
|
||||
|
||||
model = AnthropicModel(llm, api_key=api_key) if is_anthropic else OpenAIModel(llm, base_url=base_url, api_key=api_key)
|
||||
embedding_model = get_env_var('EMBEDDING_MODEL') or 'text-embedding-3-small'
|
||||
|
||||
logfire.configure(send_to_logfire='if-token-present')
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
|
||||
@dataclass
|
||||
class PydanticAIDeps:
|
||||
supabase: Client
|
||||
@ -38,41 +41,217 @@ class PydanticAIDeps:
|
||||
reasoner_output: str
|
||||
|
||||
system_prompt = """
|
||||
~~ CONTEXT: ~~
|
||||
[ROLE AND CONTEXT]
|
||||
You are a specialized AI agent engineer focused on building robust Pydantic AI agents. You have comprehensive access to the Pydantic AI documentation, including API references, usage guides, and implementation examples.
|
||||
|
||||
You are an expert at Pydantic AI - a Python AI agent framework that you have access to all the documentation to,
|
||||
including examples, an API reference, and other resources to help you build Pydantic AI agents.
|
||||
[CORE RESPONSIBILITIES]
|
||||
1. Agent Development
|
||||
- Create new agents from user requirements
|
||||
- Complete partial agent implementations
|
||||
- Optimize and debug existing agents
|
||||
- Guide users through agent specification if needed
|
||||
|
||||
~~ GOAL: ~~
|
||||
2. Documentation Integration
|
||||
- Systematically search documentation using RAG before any implementation
|
||||
- Cross-reference multiple documentation pages for comprehensive understanding
|
||||
- Validate all implementations against current best practices
|
||||
- Notify users if documentation is insufficient for any requirement
|
||||
|
||||
Your only job is to help the user create an AI agent with Pydantic AI.
|
||||
The user will describe the AI agent they want to build, or if they don't, guide them towards doing so.
|
||||
You will take their requirements, and then search through the Pydantic AI documentation with the tools provided
|
||||
to find all the necessary information to create the AI agent with correct code.
|
||||
[CODE STRUCTURE AND DELIVERABLES]
|
||||
All new agents must include these files with complete, production-ready code:
|
||||
|
||||
It's important for you to search through multiple Pydantic AI documentation pages to get all the information you need.
|
||||
Almost never stick to just one page - use RAG and the other documentation tools multiple times when you are creating
|
||||
an AI agent from scratch for the user.
|
||||
1. agent.py
|
||||
- Primary agent definition and configuration
|
||||
- Core agent logic and behaviors
|
||||
- No tool implementations allowed here
|
||||
|
||||
~~ STRUCTURE: ~~
|
||||
2. agent_tools.py
|
||||
- All tool function implementations
|
||||
- Tool configurations and setup
|
||||
- External service integrations
|
||||
|
||||
When you build an AI agent from scratch, split the agent into this files and give the code for each:
|
||||
- `agent.py`: The main agent file, which is where the Pydantic AI agent is defined.
|
||||
- `agent_tools.py`: A tools file for the agent, which is where all the tool functions are defined. Use this for more complex agents.
|
||||
- `agent_prompts.py`: A prompts file for the agent, which includes all system prompts and other prompts used by the agent. Use this when there are many prompts or large ones.
|
||||
- `.env.example`: An example `.env` file - specify each variable that the user will need to fill in and a quick comment above each one for how to do so.
|
||||
- `requirements.txt`: Don't include any versions, just the top level package names needed for the agent.
|
||||
3. agent_prompts.py
|
||||
- System prompts
|
||||
- Task-specific prompts
|
||||
- Conversation templates
|
||||
- Instruction sets
|
||||
|
||||
~~ INSTRUCTIONS: ~~
|
||||
4. .env.example
|
||||
- Required environment variables
|
||||
- Clear setup instructions in a comment above the variable for how to do so
|
||||
- API configuration templates
|
||||
|
||||
- Don't ask the user before taking an action, just do it. Always make sure you look at the documentation with the provided tools before writing any code.
|
||||
- When you first look at the documentation, always start with RAG.
|
||||
Then also always check the list of available documentation pages and retrieve the content of page(s) if it'll help.
|
||||
- Always let the user know when you didn't find the answer in the documentation or the right URL - be honest.
|
||||
- Helpful tip: when starting a new AI agent build, it's a good idea to look at the 'weather agent' in the docs as an example.
|
||||
- When starting a new AI agent build, always produce the full code for the AI agent - never tell the user to finish a tool/function.
|
||||
- When refining an existing AI agent build in a conversation, just share the code changes necessary.
|
||||
- Each time you respond to the user, ask them to let you know either if they need changes or the code looks good.
|
||||
5. requirements.txt
|
||||
- Core dependencies without versions
|
||||
- User-specified packages included
|
||||
|
||||
[DOCUMENTATION WORKFLOW]
|
||||
1. Initial Research
|
||||
- Begin with RAG search for relevant documentation
|
||||
- List all documentation pages using list_documentation_pages
|
||||
- Retrieve specific page content using get_page_content
|
||||
- Cross-reference the weather agent example for best practices
|
||||
|
||||
2. Implementation
|
||||
- Provide complete, working code implementations
|
||||
- Never leave placeholder functions
|
||||
- Include all necessary error handling
|
||||
- Implement proper logging and monitoring
|
||||
|
||||
3. Quality Assurance
|
||||
- Verify all tool implementations are complete
|
||||
- Ensure proper separation of concerns
|
||||
- Validate environment variable handling
|
||||
- Test critical path functionality
|
||||
|
||||
[INTERACTION GUIDELINES]
|
||||
- Take immediate action without asking for permission
|
||||
- Always verify documentation before implementation
|
||||
- Provide honest feedback about documentation gaps
|
||||
- Include specific enhancement suggestions
|
||||
- Request user feedback on implementations
|
||||
- Maintain code consistency across files
|
||||
|
||||
[ERROR HANDLING]
|
||||
- Implement robust error handling in all tools
|
||||
- Provide clear error messages
|
||||
- Include recovery mechanisms
|
||||
- Log important state changes
|
||||
|
||||
[BEST PRACTICES]
|
||||
- Follow Pydantic AI naming conventions
|
||||
- Implement proper type hints
|
||||
- Include comprehensive docstrings, the agent uses this to understand what tools are for.
|
||||
- Maintain clean code structure
|
||||
- Use consistent formatting
|
||||
|
||||
Here is a good example of a Pydantic AI agent:
|
||||
|
||||
```python
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import logfire
|
||||
from devtools import debug
|
||||
from httpx import AsyncClient
|
||||
|
||||
from pydantic_ai import Agent, ModelRetry, RunContext
|
||||
|
||||
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
|
||||
logfire.configure(send_to_logfire='if-token-present')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
client: AsyncClient
|
||||
weather_api_key: str | None
|
||||
geo_api_key: str | None
|
||||
|
||||
|
||||
weather_agent = Agent(
|
||||
'openai:gpt-4o',
|
||||
# 'Be concise, reply with one sentence.' is enough for some models (like openai) to use
|
||||
# the below tools appropriately, but others like anthropic and gemini require a bit more direction.
|
||||
system_prompt=(
|
||||
'Be concise, reply with one sentence.'
|
||||
'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, '
|
||||
'then use the `get_weather` tool to get the weather.'
|
||||
),
|
||||
deps_type=Deps,
|
||||
retries=2,
|
||||
)
|
||||
|
||||
|
||||
@weather_agent.tool
|
||||
async def get_lat_lng(
|
||||
ctx: RunContext[Deps], location_description: str
|
||||
) -> dict[str, float]:
|
||||
\"\"\"Get the latitude and longitude of a location.
|
||||
|
||||
Args:
|
||||
ctx: The context.
|
||||
location_description: A description of a location.
|
||||
\"\"\"
|
||||
if ctx.deps.geo_api_key is None:
|
||||
# if no API key is provided, return a dummy response (London)
|
||||
return {'lat': 51.1, 'lng': -0.1}
|
||||
|
||||
params = {
|
||||
'q': location_description,
|
||||
'api_key': ctx.deps.geo_api_key,
|
||||
}
|
||||
with logfire.span('calling geocode API', params=params) as span:
|
||||
r = await ctx.deps.client.get('https://geocode.maps.co/search', params=params)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
span.set_attribute('response', data)
|
||||
|
||||
if data:
|
||||
return {'lat': data[0]['lat'], 'lng': data[0]['lon']}
|
||||
else:
|
||||
raise ModelRetry('Could not find the location')
|
||||
|
||||
|
||||
@weather_agent.tool
|
||||
async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]:
|
||||
\"\"\"Get the weather at a location.
|
||||
|
||||
Args:
|
||||
ctx: The context.
|
||||
lat: Latitude of the location.
|
||||
lng: Longitude of the location.
|
||||
\"\"\"
|
||||
if ctx.deps.weather_api_key is None:
|
||||
# if no API key is provided, return a dummy response
|
||||
return {'temperature': '21 °C', 'description': 'Sunny'}
|
||||
|
||||
params = {
|
||||
'apikey': ctx.deps.weather_api_key,
|
||||
'location': f'{lat},{lng}',
|
||||
'units': 'metric',
|
||||
}
|
||||
with logfire.span('calling weather API', params=params) as span:
|
||||
r = await ctx.deps.client.get(
|
||||
'https://api.tomorrow.io/v4/weather/realtime', params=params
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
span.set_attribute('response', data)
|
||||
|
||||
values = data['data']['values']
|
||||
# https://docs.tomorrow.io/reference/data-layers-weather-codes
|
||||
code_lookup = {
|
||||
...
|
||||
}
|
||||
return {
|
||||
'temperature': f'{values["temperatureApparent"]:0.0f}°C',
|
||||
'description': code_lookup.get(values['weatherCode'], 'Unknown'),
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
async with AsyncClient() as client:
|
||||
# create a free API key at https://www.tomorrow.io/weather-api/
|
||||
weather_api_key = os.getenv('WEATHER_API_KEY')
|
||||
# create a free API key at https://geocode.maps.co/
|
||||
geo_api_key = os.getenv('GEO_API_KEY')
|
||||
deps = Deps(
|
||||
client=client, weather_api_key=weather_api_key, geo_api_key=geo_api_key
|
||||
)
|
||||
result = await weather_agent.run(
|
||||
'What is the weather like in London and in Wiltshire?', deps=deps
|
||||
)
|
||||
debug(result)
|
||||
print('Response:', result.data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
```
|
||||
"""
|
||||
|
||||
pydantic_ai_coder = Agent(
|
||||
@ -115,7 +294,7 @@ async def retrieve_relevant_documentation(ctx: RunContext[PydanticAIDeps], user_
|
||||
user_query: The user's question or query
|
||||
|
||||
Returns:
|
||||
A formatted string containing the top 5 most relevant documentation chunks
|
||||
A formatted string containing the top 4 most relevant documentation chunks
|
||||
"""
|
||||
try:
|
||||
# Get the embedding for the query
|
||||
@ -126,7 +305,7 @@ async def retrieve_relevant_documentation(ctx: RunContext[PydanticAIDeps], user_
|
||||
'match_site_pages',
|
||||
{
|
||||
'query_embedding': query_embedding,
|
||||
'match_count': 5,
|
||||
'match_count': 4,
|
||||
'filter': {'source': 'pydantic_ai_docs'}
|
||||
}
|
||||
).execute()
|
||||
@ -220,8 +399,9 @@ async def get_page_content(ctx: RunContext[PydanticAIDeps], url: str) -> str:
|
||||
for chunk in result.data:
|
||||
formatted_content.append(chunk['content'])
|
||||
|
||||
# Join everything together
|
||||
return "\n\n".join(formatted_content)
|
||||
# Join everything together but limit the characters in case the page is massive (there are a coule big ones)
|
||||
# This will be improved later so if the page is too big RAG will be performed on the page itself
|
||||
return "\n\n".join(formatted_content)[:20000]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving page content: {e}")
|
||||
|
||||
@ -2,16 +2,18 @@
|
||||
# OpenAI: https://api.openai.com/v1
|
||||
# Ollama (example): http://localhost:11434/v1
|
||||
# OpenRouter: https://openrouter.ai/api/v1
|
||||
# Anthropic: https://api.anthropic.com/v1
|
||||
BASE_URL=
|
||||
|
||||
# For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
|
||||
# For Anthropic: https://console.anthropic.com/account/keys
|
||||
# For OpenRouter: https://openrouter.ai/keys
|
||||
# For Ollama, no need to set this unless you specifically configured an API key
|
||||
LLM_API_KEY=
|
||||
|
||||
# Get your Open AI API Key by following these instructions -
|
||||
# https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key
|
||||
# Even if using OpenRouter, you still need to set this for the embedding model.
|
||||
# Even if using Anthropic or OpenRouter, you still need to set this for the embedding model.
|
||||
# No need to set this if using Ollama.
|
||||
OPENAI_API_KEY=
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ This version continues to support both local LLMs with Ollama and cloud-based LL
|
||||
- Docker (optional but preferred)
|
||||
- Python 3.11+
|
||||
- Supabase account (for vector database)
|
||||
- OpenAI/OpenRouter API key or Ollama for local LLMs
|
||||
- OpenAI/OpenRouter/Anthropic API key or Ollama for local LLMs
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from pydantic_ai.models.anthropic import AnthropicModel
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from pydantic_ai import Agent, RunContext
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
@ -31,21 +32,29 @@ logfire.configure(send_to_logfire='never')
|
||||
|
||||
base_url = get_env_var('BASE_URL') or 'https://api.openai.com/v1'
|
||||
api_key = get_env_var('LLM_API_KEY') or 'no-llm-api-key-provided'
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
reasoner_llm_model = get_env_var('REASONER_MODEL') or 'o3-mini'
|
||||
is_anthropic = "anthropic" in base_url.lower()
|
||||
is_openai = "openai" in base_url.lower()
|
||||
|
||||
reasoner_llm_model_name = get_env_var('REASONER_MODEL') or 'o3-mini'
|
||||
reasoner_llm_model = AnthropicModel(reasoner_llm_model_name, api_key=api_key) if is_anthropic else OpenAIModel(reasoner_llm_model_name, base_url=base_url, api_key=api_key)
|
||||
|
||||
reasoner = Agent(
|
||||
OpenAIModel(reasoner_llm_model, base_url=base_url, api_key=api_key),
|
||||
reasoner_llm_model,
|
||||
system_prompt='You are an expert at coding AI agents with Pydantic AI and defining the scope for doing so.',
|
||||
)
|
||||
|
||||
primary_llm_model = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
primary_llm_model_name = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
primary_llm_model = AnthropicModel(primary_llm_model_name, api_key=api_key) if is_anthropic else OpenAIModel(primary_llm_model_name, base_url=base_url, api_key=api_key)
|
||||
|
||||
router_agent = Agent(
|
||||
OpenAIModel(primary_llm_model, base_url=base_url, api_key=api_key),
|
||||
primary_llm_model,
|
||||
system_prompt='Your job is to route the user message either to the end of the conversation or to continue coding the AI agent.',
|
||||
)
|
||||
|
||||
end_conversation_agent = Agent(
|
||||
OpenAIModel(primary_llm_model, base_url=base_url, api_key=api_key),
|
||||
primary_llm_model,
|
||||
system_prompt='Your job is to end a conversation for creating an AI agent by giving instructions for how to execute the agent and they saying a nice goodbye to the user.',
|
||||
)
|
||||
|
||||
@ -124,7 +133,7 @@ async def coder_agent(state: AgentState, writer):
|
||||
message_history.extend(ModelMessagesTypeAdapter.validate_json(message_row))
|
||||
|
||||
# Run the agent in a stream
|
||||
if is_ollama:
|
||||
if not is_openai:
|
||||
writer = get_stream_writer()
|
||||
result = await pydantic_ai_coder.run(state['latest_user_message'], deps=deps, message_history= message_history)
|
||||
writer(result.data)
|
||||
@ -178,7 +187,7 @@ async def finish_conversation(state: AgentState, writer):
|
||||
message_history.extend(ModelMessagesTypeAdapter.validate_json(message_row))
|
||||
|
||||
# Run the agent in a stream
|
||||
if is_ollama:
|
||||
if not is_openai:
|
||||
writer = get_stream_writer()
|
||||
result = await end_conversation_agent.run(state['latest_user_message'], message_history= message_history)
|
||||
writer(result.data)
|
||||
|
||||
@ -11,6 +11,7 @@ import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pydantic import BaseModel
|
||||
from pydantic_ai import Agent, ModelRetry, RunContext
|
||||
from pydantic_ai.models.anthropic import AnthropicModel
|
||||
from pydantic_ai.models.openai import OpenAIModel
|
||||
from openai import AsyncOpenAI
|
||||
from supabase import Client
|
||||
@ -24,13 +25,15 @@ load_dotenv()
|
||||
llm = get_env_var('PRIMARY_MODEL') or 'gpt-4o-mini'
|
||||
base_url = get_env_var('BASE_URL') or 'https://api.openai.com/v1'
|
||||
api_key = get_env_var('LLM_API_KEY') or 'no-llm-api-key-provided'
|
||||
model = OpenAIModel(llm, base_url=base_url, api_key=api_key)
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
is_anthropic = "anthropic" in base_url.lower()
|
||||
|
||||
model = AnthropicModel(llm, api_key=api_key) if is_anthropic else OpenAIModel(llm, base_url=base_url, api_key=api_key)
|
||||
embedding_model = get_env_var('EMBEDDING_MODEL') or 'text-embedding-3-small'
|
||||
|
||||
logfire.configure(send_to_logfire='if-token-present')
|
||||
|
||||
is_ollama = "localhost" in base_url.lower()
|
||||
|
||||
@dataclass
|
||||
class PydanticAIDeps:
|
||||
supabase: Client
|
||||
@ -38,41 +41,217 @@ class PydanticAIDeps:
|
||||
reasoner_output: str
|
||||
|
||||
system_prompt = """
|
||||
~~ CONTEXT: ~~
|
||||
[ROLE AND CONTEXT]
|
||||
You are a specialized AI agent engineer focused on building robust Pydantic AI agents. You have comprehensive access to the Pydantic AI documentation, including API references, usage guides, and implementation examples.
|
||||
|
||||
You are an expert at Pydantic AI - a Python AI agent framework that you have access to all the documentation to,
|
||||
including examples, an API reference, and other resources to help you build Pydantic AI agents.
|
||||
[CORE RESPONSIBILITIES]
|
||||
1. Agent Development
|
||||
- Create new agents from user requirements
|
||||
- Complete partial agent implementations
|
||||
- Optimize and debug existing agents
|
||||
- Guide users through agent specification if needed
|
||||
|
||||
~~ GOAL: ~~
|
||||
2. Documentation Integration
|
||||
- Systematically search documentation using RAG before any implementation
|
||||
- Cross-reference multiple documentation pages for comprehensive understanding
|
||||
- Validate all implementations against current best practices
|
||||
- Notify users if documentation is insufficient for any requirement
|
||||
|
||||
Your only job is to help the user create an AI agent with Pydantic AI.
|
||||
The user will describe the AI agent they want to build, or if they don't, guide them towards doing so.
|
||||
You will take their requirements, and then search through the Pydantic AI documentation with the tools provided
|
||||
to find all the necessary information to create the AI agent with correct code.
|
||||
[CODE STRUCTURE AND DELIVERABLES]
|
||||
All new agents must include these files with complete, production-ready code:
|
||||
|
||||
It's important for you to search through multiple Pydantic AI documentation pages to get all the information you need.
|
||||
Almost never stick to just one page - use RAG and the other documentation tools multiple times when you are creating
|
||||
an AI agent from scratch for the user.
|
||||
1. agent.py
|
||||
- Primary agent definition and configuration
|
||||
- Core agent logic and behaviors
|
||||
- No tool implementations allowed here
|
||||
|
||||
~~ STRUCTURE: ~~
|
||||
2. agent_tools.py
|
||||
- All tool function implementations
|
||||
- Tool configurations and setup
|
||||
- External service integrations
|
||||
|
||||
When you build an AI agent from scratch, split the agent into this files and give the code for each:
|
||||
- `agent.py`: The main agent file, which is where the Pydantic AI agent is defined.
|
||||
- `agent_tools.py`: A tools file for the agent, which is where all the tool functions are defined. Use this for more complex agents.
|
||||
- `agent_prompts.py`: A prompts file for the agent, which includes all system prompts and other prompts used by the agent. Use this when there are many prompts or large ones.
|
||||
- `.env.example`: An example `.env` file - specify each variable that the user will need to fill in and a quick comment above each one for how to do so.
|
||||
- `requirements.txt`: Don't include any versions, just the top level package names needed for the agent.
|
||||
3. agent_prompts.py
|
||||
- System prompts
|
||||
- Task-specific prompts
|
||||
- Conversation templates
|
||||
- Instruction sets
|
||||
|
||||
~~ INSTRUCTIONS: ~~
|
||||
4. .env.example
|
||||
- Required environment variables
|
||||
- Clear setup instructions in a comment above the variable for how to do so
|
||||
- API configuration templates
|
||||
|
||||
- Don't ask the user before taking an action, just do it. Always make sure you look at the documentation with the provided tools before writing any code.
|
||||
- When you first look at the documentation, always start with RAG.
|
||||
Then also always check the list of available documentation pages and retrieve the content of page(s) if it'll help.
|
||||
- Always let the user know when you didn't find the answer in the documentation or the right URL - be honest.
|
||||
- Helpful tip: when starting a new AI agent build, it's a good idea to look at the 'weather agent' in the docs as an example.
|
||||
- When starting a new AI agent build, always produce the full code for the AI agent - never tell the user to finish a tool/function.
|
||||
- When refining an existing AI agent build in a conversation, just share the code changes necessary.
|
||||
- Each time you respond to the user, ask them to let you know either if they need changes or the code looks good.
|
||||
5. requirements.txt
|
||||
- Core dependencies without versions
|
||||
- User-specified packages included
|
||||
|
||||
[DOCUMENTATION WORKFLOW]
|
||||
1. Initial Research
|
||||
- Begin with RAG search for relevant documentation
|
||||
- List all documentation pages using list_documentation_pages
|
||||
- Retrieve specific page content using get_page_content
|
||||
- Cross-reference the weather agent example for best practices
|
||||
|
||||
2. Implementation
|
||||
- Provide complete, working code implementations
|
||||
- Never leave placeholder functions
|
||||
- Include all necessary error handling
|
||||
- Implement proper logging and monitoring
|
||||
|
||||
3. Quality Assurance
|
||||
- Verify all tool implementations are complete
|
||||
- Ensure proper separation of concerns
|
||||
- Validate environment variable handling
|
||||
- Test critical path functionality
|
||||
|
||||
[INTERACTION GUIDELINES]
|
||||
- Take immediate action without asking for permission
|
||||
- Always verify documentation before implementation
|
||||
- Provide honest feedback about documentation gaps
|
||||
- Include specific enhancement suggestions
|
||||
- Request user feedback on implementations
|
||||
- Maintain code consistency across files
|
||||
|
||||
[ERROR HANDLING]
|
||||
- Implement robust error handling in all tools
|
||||
- Provide clear error messages
|
||||
- Include recovery mechanisms
|
||||
- Log important state changes
|
||||
|
||||
[BEST PRACTICES]
|
||||
- Follow Pydantic AI naming conventions
|
||||
- Implement proper type hints
|
||||
- Include comprehensive docstrings, the agent uses this to understand what tools are for.
|
||||
- Maintain clean code structure
|
||||
- Use consistent formatting
|
||||
|
||||
Here is a good example of a Pydantic AI agent:
|
||||
|
||||
```python
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import logfire
|
||||
from devtools import debug
|
||||
from httpx import AsyncClient
|
||||
|
||||
from pydantic_ai import Agent, ModelRetry, RunContext
|
||||
|
||||
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
|
||||
logfire.configure(send_to_logfire='if-token-present')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Deps:
|
||||
client: AsyncClient
|
||||
weather_api_key: str | None
|
||||
geo_api_key: str | None
|
||||
|
||||
|
||||
weather_agent = Agent(
|
||||
'openai:gpt-4o',
|
||||
# 'Be concise, reply with one sentence.' is enough for some models (like openai) to use
|
||||
# the below tools appropriately, but others like anthropic and gemini require a bit more direction.
|
||||
system_prompt=(
|
||||
'Be concise, reply with one sentence.'
|
||||
'Use the `get_lat_lng` tool to get the latitude and longitude of the locations, '
|
||||
'then use the `get_weather` tool to get the weather.'
|
||||
),
|
||||
deps_type=Deps,
|
||||
retries=2,
|
||||
)
|
||||
|
||||
|
||||
@weather_agent.tool
|
||||
async def get_lat_lng(
|
||||
ctx: RunContext[Deps], location_description: str
|
||||
) -> dict[str, float]:
|
||||
\"\"\"Get the latitude and longitude of a location.
|
||||
|
||||
Args:
|
||||
ctx: The context.
|
||||
location_description: A description of a location.
|
||||
\"\"\"
|
||||
if ctx.deps.geo_api_key is None:
|
||||
# if no API key is provided, return a dummy response (London)
|
||||
return {'lat': 51.1, 'lng': -0.1}
|
||||
|
||||
params = {
|
||||
'q': location_description,
|
||||
'api_key': ctx.deps.geo_api_key,
|
||||
}
|
||||
with logfire.span('calling geocode API', params=params) as span:
|
||||
r = await ctx.deps.client.get('https://geocode.maps.co/search', params=params)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
span.set_attribute('response', data)
|
||||
|
||||
if data:
|
||||
return {'lat': data[0]['lat'], 'lng': data[0]['lon']}
|
||||
else:
|
||||
raise ModelRetry('Could not find the location')
|
||||
|
||||
|
||||
@weather_agent.tool
|
||||
async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]:
|
||||
\"\"\"Get the weather at a location.
|
||||
|
||||
Args:
|
||||
ctx: The context.
|
||||
lat: Latitude of the location.
|
||||
lng: Longitude of the location.
|
||||
\"\"\"
|
||||
if ctx.deps.weather_api_key is None:
|
||||
# if no API key is provided, return a dummy response
|
||||
return {'temperature': '21 °C', 'description': 'Sunny'}
|
||||
|
||||
params = {
|
||||
'apikey': ctx.deps.weather_api_key,
|
||||
'location': f'{lat},{lng}',
|
||||
'units': 'metric',
|
||||
}
|
||||
with logfire.span('calling weather API', params=params) as span:
|
||||
r = await ctx.deps.client.get(
|
||||
'https://api.tomorrow.io/v4/weather/realtime', params=params
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
span.set_attribute('response', data)
|
||||
|
||||
values = data['data']['values']
|
||||
# https://docs.tomorrow.io/reference/data-layers-weather-codes
|
||||
code_lookup = {
|
||||
...
|
||||
}
|
||||
return {
|
||||
'temperature': f'{values["temperatureApparent"]:0.0f}°C',
|
||||
'description': code_lookup.get(values['weatherCode'], 'Unknown'),
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
async with AsyncClient() as client:
|
||||
# create a free API key at https://www.tomorrow.io/weather-api/
|
||||
weather_api_key = os.getenv('WEATHER_API_KEY')
|
||||
# create a free API key at https://geocode.maps.co/
|
||||
geo_api_key = os.getenv('GEO_API_KEY')
|
||||
deps = Deps(
|
||||
client=client, weather_api_key=weather_api_key, geo_api_key=geo_api_key
|
||||
)
|
||||
result = await weather_agent.run(
|
||||
'What is the weather like in London and in Wiltshire?', deps=deps
|
||||
)
|
||||
debug(result)
|
||||
print('Response:', result.data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
```
|
||||
"""
|
||||
|
||||
pydantic_ai_coder = Agent(
|
||||
@ -115,7 +294,7 @@ async def retrieve_relevant_documentation(ctx: RunContext[PydanticAIDeps], user_
|
||||
user_query: The user's question or query
|
||||
|
||||
Returns:
|
||||
A formatted string containing the top 5 most relevant documentation chunks
|
||||
A formatted string containing the top 4 most relevant documentation chunks
|
||||
"""
|
||||
try:
|
||||
# Get the embedding for the query
|
||||
@ -126,7 +305,7 @@ async def retrieve_relevant_documentation(ctx: RunContext[PydanticAIDeps], user_
|
||||
'match_site_pages',
|
||||
{
|
||||
'query_embedding': query_embedding,
|
||||
'match_count': 5,
|
||||
'match_count': 4,
|
||||
'filter': {'source': 'pydantic_ai_docs'}
|
||||
}
|
||||
).execute()
|
||||
@ -220,8 +399,9 @@ async def get_page_content(ctx: RunContext[PydanticAIDeps], url: str) -> str:
|
||||
for chunk in result.data:
|
||||
formatted_content.append(chunk['content'])
|
||||
|
||||
# Join everything together
|
||||
return "\n\n".join(formatted_content)
|
||||
# Join everything together but limit the characters in case the page is massive (there are a coule big ones)
|
||||
# This will be improved later so if the page is too big RAG will be performed on the page itself
|
||||
return "\n\n".join(formatted_content)[:20000]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving page content: {e}")
|
||||
|
||||
@ -21,6 +21,7 @@ from openai import AsyncOpenAI
|
||||
from supabase import Client, create_client
|
||||
from dotenv import load_dotenv
|
||||
from utils.utils import get_env_var, save_env_var, write_to_log
|
||||
from future_enhancements import future_enhancements_tab
|
||||
|
||||
# Import all the message part classes
|
||||
from pydantic_ai.messages import (
|
||||
@ -71,13 +72,13 @@ st.set_page_config(
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
# Set custom theme colors to match Archon logo (pink and green)
|
||||
# Primary color (pink) and secondary color (green)
|
||||
# Set custom theme colors to match Archon logo (green and pink)
|
||||
# Primary color (green) and secondary color (pink)
|
||||
st.markdown("""
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #00CC99; /* Pink */
|
||||
--secondary-color: #FF69B4; /* Green */
|
||||
--primary-color: #00CC99; /* Green */
|
||||
--secondary-color: #EB2D8C; /* Pink */
|
||||
--text-color: #262730;
|
||||
}
|
||||
|
||||
@ -487,9 +488,9 @@ def intro_tab():
|
||||
create_new_tab_button("Go to the Documentation Section (New Tab)", "Documentation", key="goto_docs", use_container_width=True)
|
||||
|
||||
# Step 4: Agent Service
|
||||
with st.expander("Step 4: Agent Service Setup", expanded=False):
|
||||
with st.expander("Step 4: Agent Service Setup (for MCP)", expanded=False):
|
||||
st.markdown("""
|
||||
### Agent Service Setup
|
||||
### MCP Agent Service Setup
|
||||
|
||||
Start the graph service for agent generation:
|
||||
|
||||
@ -879,8 +880,8 @@ def show_manual_sql_instructions(sql, recreate=False):
|
||||
|
||||
def agent_service_tab():
|
||||
"""Display the agent service interface for managing the graph service"""
|
||||
st.header("Agent Service")
|
||||
st.write("Start, restart, and monitor the Archon agent service.")
|
||||
st.header("MCP Agent Service")
|
||||
st.write("Start, restart, and monitor the Archon agent service for MCP.")
|
||||
|
||||
# Initialize session state variables if they don't exist
|
||||
if "service_process" not in st.session_state:
|
||||
@ -1102,17 +1103,18 @@ def environment_tab():
|
||||
st.write("- Configure your environment variables for Archon. These settings will be saved and used for future sessions.")
|
||||
st.write("- NOTE: Press 'enter' to save after inputting a variable, otherwise click the 'save' button at the bottom.")
|
||||
st.write("- HELP: Hover over the '?' icon on the right for each environment variable for help/examples.")
|
||||
st.warning("⚠️ If your agent service for MCP is already running, you'll need to restart it after changing environment variables.")
|
||||
|
||||
# Define environment variables and their descriptions from .env.example
|
||||
env_vars = {
|
||||
"BASE_URL": {
|
||||
"description": "Base URL for the OpenAI instance (default is https://api.openai.com/v1)",
|
||||
"help": "OpenAI: https://api.openai.com/v1\n\nOllama (example): http://localhost:11434/v1\n\nOpenRouter: https://openrouter.ai/api/v1",
|
||||
"help": "OpenAI: https://api.openai.com/v1\n\n\n\nAnthropic: https://api.anthropic.com/v1\n\nOllama (example): http://localhost:11434/v1\n\nOpenRouter: https://openrouter.ai/api/v1",
|
||||
"sensitive": False
|
||||
},
|
||||
"LLM_API_KEY": {
|
||||
"description": "API key for your LLM provider",
|
||||
"help": "For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key\n\nFor OpenRouter: https://openrouter.ai/keys\n\nFor Ollama, no need to set this unless you specifically configured an API key",
|
||||
"help": "For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key\n\nFor Anthropic: https://console.anthropic.com/account/keys\n\nFor OpenRouter: https://openrouter.ai/keys\n\nFor Ollama, no need to set this unless you specifically configured an API key",
|
||||
"sensitive": True
|
||||
},
|
||||
"OPENAI_API_KEY": {
|
||||
@ -1207,7 +1209,7 @@ async def main():
|
||||
query_params = st.query_params
|
||||
if "tab" in query_params:
|
||||
tab_name = query_params["tab"]
|
||||
if tab_name in ["Intro", "Chat", "Environment", "Database", "Documentation", "Agent Service", "MCP"]:
|
||||
if tab_name in ["Intro", "Chat", "Environment", "Database", "Documentation", "Agent Service", "MCP", "Future Enhancements"]:
|
||||
st.session_state.selected_tab = tab_name
|
||||
|
||||
# Add sidebar navigation
|
||||
@ -1229,6 +1231,7 @@ async def main():
|
||||
docs_button = st.button("Documentation", use_container_width=True, key="docs_button")
|
||||
service_button = st.button("Agent Service", use_container_width=True, key="service_button")
|
||||
mcp_button = st.button("MCP", use_container_width=True, key="mcp_button")
|
||||
future_enhancements_button = st.button("Future Enhancements", use_container_width=True, key="future_enhancements_button")
|
||||
|
||||
# Update selected tab based on button clicks
|
||||
if intro_button:
|
||||
@ -1245,6 +1248,8 @@ async def main():
|
||||
st.session_state.selected_tab = "Database"
|
||||
elif docs_button:
|
||||
st.session_state.selected_tab = "Documentation"
|
||||
elif future_enhancements_button:
|
||||
st.session_state.selected_tab = "Future Enhancements"
|
||||
|
||||
# Display the selected tab
|
||||
if st.session_state.selected_tab == "Intro":
|
||||
@ -1268,6 +1273,9 @@ async def main():
|
||||
elif st.session_state.selected_tab == "Documentation":
|
||||
st.title("Archon - Documentation")
|
||||
documentation_tab()
|
||||
elif st.session_state.selected_tab == "Future Enhancements":
|
||||
st.title("Archon - Future Enhancements")
|
||||
future_enhancements_tab()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
@ -1103,17 +1103,18 @@ def environment_tab():
|
||||
st.write("- Configure your environment variables for Archon. These settings will be saved and used for future sessions.")
|
||||
st.write("- NOTE: Press 'enter' to save after inputting a variable, otherwise click the 'save' button at the bottom.")
|
||||
st.write("- HELP: Hover over the '?' icon on the right for each environment variable for help/examples.")
|
||||
st.warning("⚠️ If your agent service for MCP is already running, you'll need to restart it after changing environment variables.")
|
||||
|
||||
# Define environment variables and their descriptions from .env.example
|
||||
env_vars = {
|
||||
"BASE_URL": {
|
||||
"description": "Base URL for the OpenAI instance (default is https://api.openai.com/v1)",
|
||||
"help": "OpenAI: https://api.openai.com/v1\n\nOllama (example): http://localhost:11434/v1\n\nOpenRouter: https://openrouter.ai/api/v1",
|
||||
"help": "OpenAI: https://api.openai.com/v1\n\n\n\nAnthropic: https://api.anthropic.com/v1\n\nOllama (example): http://localhost:11434/v1\n\nOpenRouter: https://openrouter.ai/api/v1",
|
||||
"sensitive": False
|
||||
},
|
||||
"LLM_API_KEY": {
|
||||
"description": "API key for your LLM provider",
|
||||
"help": "For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key\n\nFor OpenRouter: https://openrouter.ai/keys\n\nFor Ollama, no need to set this unless you specifically configured an API key",
|
||||
"help": "For OpenAI: https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key\n\nFor Anthropic: https://console.anthropic.com/account/keys\n\nFor OpenRouter: https://openrouter.ai/keys\n\nFor Ollama, no need to set this unless you specifically configured an API key",
|
||||
"sensitive": True
|
||||
},
|
||||
"OPENAI_API_KEY": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user