pixie-examples 0.1.1.dev3__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.
- examples/__init__.py +0 -0
- examples/langchain/README.md +39 -0
- examples/langchain/__init__.py +1 -0
- examples/langchain/basic_agent.py +100 -0
- examples/langchain/customer_support.py +238 -0
- examples/langchain/personal_assistant.py +163 -0
- examples/langchain/sql_agent.py +176 -0
- examples/langgraph/__init__.py +0 -0
- examples/langgraph/langgraph_rag.py +241 -0
- examples/langgraph/langgraph_sql_agent.py +218 -0
- examples/openai_agents_sdk/README.md +299 -0
- examples/openai_agents_sdk/__init__.py +0 -0
- examples/openai_agents_sdk/customer_service.py +258 -0
- examples/openai_agents_sdk/financial_research_agent.py +328 -0
- examples/openai_agents_sdk/llm_as_a_judge.py +108 -0
- examples/openai_agents_sdk/routing.py +177 -0
- examples/pydantic_ai/.env.example +26 -0
- examples/pydantic_ai/README.md +246 -0
- examples/pydantic_ai/__init__.py +0 -0
- examples/pydantic_ai/bank_support.py +154 -0
- examples/pydantic_ai/flight_booking.py +250 -0
- examples/pydantic_ai/question_graph.py +152 -0
- examples/pydantic_ai/sql_gen.py +182 -0
- examples/pydantic_ai/structured_output.py +64 -0
- examples/quickstart/__init__.py +0 -0
- examples/quickstart/chatbot.py +25 -0
- examples/quickstart/sleepy_poet.py +96 -0
- examples/quickstart/weather_agent.py +110 -0
- examples/sql_utils.py +241 -0
- pixie_examples-0.1.1.dev3.dist-info/METADATA +113 -0
- pixie_examples-0.1.1.dev3.dist-info/RECORD +33 -0
- pixie_examples-0.1.1.dev3.dist-info/WHEEL +4 -0
- pixie_examples-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Example of a graph-based state machine for asking and evaluating questions.
|
|
2
|
+
|
|
3
|
+
This demonstrates using pydantic_graph for complex control flow.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic_ai import Agent, ModelMessage, format_as_xml
|
|
10
|
+
from pydantic_graph import (
|
|
11
|
+
BaseNode,
|
|
12
|
+
End,
|
|
13
|
+
Graph,
|
|
14
|
+
GraphRunContext,
|
|
15
|
+
)
|
|
16
|
+
import pixie
|
|
17
|
+
|
|
18
|
+
# Agent for asking questions
|
|
19
|
+
ask_agent = Agent("openai:gpt-4o-mini", output_type=str)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class QuestionState:
|
|
24
|
+
question: str | None = None
|
|
25
|
+
answer: str | None = None
|
|
26
|
+
ask_agent_messages: list[ModelMessage] = field(default_factory=list)
|
|
27
|
+
evaluate_agent_messages: list[ModelMessage] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Ask(BaseNode[QuestionState]):
|
|
32
|
+
"""Generate a question using the AI."""
|
|
33
|
+
|
|
34
|
+
async def run(self, ctx: GraphRunContext[QuestionState]) -> "Answer":
|
|
35
|
+
agent_result = await ask_agent.run(
|
|
36
|
+
"Ask a simple question with a single correct answer.",
|
|
37
|
+
message_history=ctx.state.ask_agent_messages,
|
|
38
|
+
)
|
|
39
|
+
ctx.state.ask_agent_messages += agent_result.all_messages()
|
|
40
|
+
ctx.state.question = agent_result.output
|
|
41
|
+
ctx.state.answer = None
|
|
42
|
+
return Answer(agent_result.output)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Answer(BaseNode[QuestionState]):
|
|
47
|
+
"""Wait for user to provide an answer."""
|
|
48
|
+
|
|
49
|
+
question: str
|
|
50
|
+
|
|
51
|
+
async def run(self, ctx: GraphRunContext[QuestionState]) -> "Evaluate":
|
|
52
|
+
if ctx.state.answer is None:
|
|
53
|
+
raise ValueError("Answer need to be set after Ask() node in the main loop.")
|
|
54
|
+
return Evaluate(ctx.state.answer)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EvaluationOutput(BaseModel, use_attribute_docstrings=True):
|
|
58
|
+
correct: bool
|
|
59
|
+
"""Whether the answer is correct."""
|
|
60
|
+
comment: str
|
|
61
|
+
"""Comment on the answer, reprimand the user if the answer is wrong."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Agent for evaluating answers
|
|
65
|
+
evaluate_agent = Agent(
|
|
66
|
+
"openai:gpt-4o-mini",
|
|
67
|
+
output_type=EvaluationOutput,
|
|
68
|
+
system_prompt="Given a question and answer, evaluate if the answer is correct.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Evaluate(BaseNode[QuestionState, None, str]):
|
|
74
|
+
"""Evaluate the user's answer."""
|
|
75
|
+
|
|
76
|
+
answer: str
|
|
77
|
+
|
|
78
|
+
async def run(
|
|
79
|
+
self,
|
|
80
|
+
ctx: GraphRunContext[QuestionState],
|
|
81
|
+
) -> End[str] | "Reprimand":
|
|
82
|
+
assert ctx.state.question is not None
|
|
83
|
+
agent_result = await evaluate_agent.run(
|
|
84
|
+
format_as_xml({"question": ctx.state.question, "answer": self.answer}),
|
|
85
|
+
message_history=ctx.state.evaluate_agent_messages,
|
|
86
|
+
)
|
|
87
|
+
ctx.state.evaluate_agent_messages += agent_result.all_messages()
|
|
88
|
+
if agent_result.output.correct:
|
|
89
|
+
return End(agent_result.output.comment)
|
|
90
|
+
else:
|
|
91
|
+
return Reprimand(agent_result.output.comment)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Reprimand(BaseNode[QuestionState]):
|
|
96
|
+
"""Tell the user they got it wrong and ask another question."""
|
|
97
|
+
|
|
98
|
+
comment: str
|
|
99
|
+
|
|
100
|
+
async def run(self, ctx: GraphRunContext[QuestionState]) -> Ask:
|
|
101
|
+
ctx.state.question = None
|
|
102
|
+
return Ask()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Create the question graph
|
|
106
|
+
_question_graph = Graph(
|
|
107
|
+
nodes=(Ask, Answer, Evaluate, Reprimand), state_type=QuestionState
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@pixie.app
|
|
112
|
+
async def pydantic_ai_question_graph() -> pixie.PixieGenerator[str, str]:
|
|
113
|
+
"""Interactive Q&A game using graph-based state machine.
|
|
114
|
+
|
|
115
|
+
The AI asks questions, the user answers, and the AI evaluates.
|
|
116
|
+
If wrong, user gets reprimanded and a new question is asked.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
yield "🧠 Welcome to the Q&A Challenge!"
|
|
120
|
+
yield "I'll ask you questions and evaluate your answers.\n"
|
|
121
|
+
|
|
122
|
+
state = QuestionState()
|
|
123
|
+
|
|
124
|
+
# Use the graph's run_sync method which handles node execution
|
|
125
|
+
async with _question_graph.iter(Ask(), state=state) as graph_ctx:
|
|
126
|
+
while True:
|
|
127
|
+
node = await graph_ctx.next()
|
|
128
|
+
|
|
129
|
+
# Handle different node types
|
|
130
|
+
if isinstance(node, Ask):
|
|
131
|
+
yield "🤔 Generating a question..."
|
|
132
|
+
|
|
133
|
+
elif isinstance(node, Answer):
|
|
134
|
+
# Ask user for their answer
|
|
135
|
+
yield f"\n❓ Question: {node.question}"
|
|
136
|
+
yield "What is your answer?"
|
|
137
|
+
state.answer = yield pixie.InputRequired(str)
|
|
138
|
+
# Continue with the user's answer by passing it to the next iteration
|
|
139
|
+
# The graph will automatically move to Evaluate with this answer
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
elif isinstance(node, Evaluate):
|
|
143
|
+
yield "⏳ Evaluating your answer..."
|
|
144
|
+
|
|
145
|
+
elif isinstance(node, Reprimand):
|
|
146
|
+
yield f"\n❌ {node.comment}"
|
|
147
|
+
yield "\n🔄 Let's try another question...\n"
|
|
148
|
+
|
|
149
|
+
elif isinstance(node, End):
|
|
150
|
+
yield f"\n✅ {node.data}"
|
|
151
|
+
yield "\n🎉 Congratulations! You got it right!"
|
|
152
|
+
return
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Example demonstrating SQL generation using Pydantic AI.
|
|
2
|
+
|
|
3
|
+
This shows multi-step workflow with validation and structured output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import date
|
|
8
|
+
from typing import Annotated, TypeAlias
|
|
9
|
+
|
|
10
|
+
from annotated_types import MinLen
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
from pydantic_ai import Agent, ModelRetry, RunContext, format_as_xml
|
|
13
|
+
import pixie
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Simulated database schema
|
|
17
|
+
DB_SCHEMA = """
|
|
18
|
+
CREATE TABLE records (
|
|
19
|
+
id bigint PRIMARY KEY,
|
|
20
|
+
created_at timestamptz,
|
|
21
|
+
start_timestamp timestamptz,
|
|
22
|
+
end_timestamp timestamptz,
|
|
23
|
+
trace_id text,
|
|
24
|
+
span_id text,
|
|
25
|
+
parent_span_id text,
|
|
26
|
+
level log_level,
|
|
27
|
+
span_name text,
|
|
28
|
+
message text,
|
|
29
|
+
attributes_json_schema text,
|
|
30
|
+
attributes jsonb,
|
|
31
|
+
tags text[],
|
|
32
|
+
is_exception boolean,
|
|
33
|
+
otel_status_message text,
|
|
34
|
+
service_name text
|
|
35
|
+
);
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
SQL_EXAMPLES = [
|
|
39
|
+
{
|
|
40
|
+
"request": "show me records where foobar is false",
|
|
41
|
+
"response": "SELECT * FROM records WHERE attributes->>'foobar' = false",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"request": 'show me records where attributes include the key "foobar"',
|
|
45
|
+
"response": "SELECT * FROM records WHERE attributes ? 'foobar'",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"request": "show me records from yesterday",
|
|
49
|
+
"response": "SELECT * FROM records WHERE start_timestamp::date > CURRENT_TIMESTAMP - INTERVAL '1 day'",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"request": 'show me error records with the tag "foobar"',
|
|
53
|
+
"response": "SELECT * FROM records WHERE level = 'error' and 'foobar' = ANY(tags)",
|
|
54
|
+
},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Deps:
|
|
60
|
+
"""Dependencies for SQL generation (simulated database connection)."""
|
|
61
|
+
|
|
62
|
+
validate_sql: bool = True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Success(BaseModel):
|
|
66
|
+
"""Response when SQL could be successfully generated."""
|
|
67
|
+
|
|
68
|
+
sql_query: Annotated[str, MinLen(1)]
|
|
69
|
+
explanation: str = Field(
|
|
70
|
+
"", description="Explanation of the SQL query, as markdown"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class InvalidRequest(BaseModel):
|
|
75
|
+
"""Response when the user input didn't include enough information to generate SQL."""
|
|
76
|
+
|
|
77
|
+
error_message: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Response: TypeAlias = Success | InvalidRequest
|
|
81
|
+
|
|
82
|
+
agent = Agent[Deps, Response](
|
|
83
|
+
"openai:gpt-4o-mini",
|
|
84
|
+
output_type=Response, # type: ignore
|
|
85
|
+
deps_type=Deps,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@agent.system_prompt
|
|
90
|
+
async def system_prompt() -> str:
|
|
91
|
+
return f"""\
|
|
92
|
+
Given the following PostgreSQL table of records, your job is to
|
|
93
|
+
write a SQL query that suits the user's request.
|
|
94
|
+
|
|
95
|
+
Database schema:
|
|
96
|
+
|
|
97
|
+
{DB_SCHEMA}
|
|
98
|
+
|
|
99
|
+
today's date = {date.today()}
|
|
100
|
+
|
|
101
|
+
{format_as_xml(SQL_EXAMPLES)}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@agent.output_validator
|
|
106
|
+
async def validate_output(ctx: RunContext[Deps], output: Response) -> Response:
|
|
107
|
+
if isinstance(output, InvalidRequest):
|
|
108
|
+
return output
|
|
109
|
+
|
|
110
|
+
# Clean up the SQL query
|
|
111
|
+
output.sql_query = output.sql_query.replace("\\", "")
|
|
112
|
+
if not output.sql_query.upper().startswith("SELECT"):
|
|
113
|
+
raise ModelRetry("Please create a SELECT query")
|
|
114
|
+
|
|
115
|
+
# In a real scenario, we'd validate against an actual database
|
|
116
|
+
# For now, we'll do basic validation
|
|
117
|
+
if ctx.deps.validate_sql:
|
|
118
|
+
# Check for dangerous operations
|
|
119
|
+
upper_query = output.sql_query.upper()
|
|
120
|
+
if any(
|
|
121
|
+
danger in upper_query
|
|
122
|
+
for danger in ["DROP", "DELETE", "UPDATE", "INSERT", "TRUNCATE"]
|
|
123
|
+
):
|
|
124
|
+
raise ModelRetry("Only SELECT queries are allowed")
|
|
125
|
+
|
|
126
|
+
# Check for required table reference
|
|
127
|
+
if "FROM RECORDS" not in upper_query and 'FROM "RECORDS"' not in upper_query:
|
|
128
|
+
raise ModelRetry("Query must reference the records table")
|
|
129
|
+
|
|
130
|
+
return output
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pixie.app
|
|
134
|
+
async def pydantic_ai_sql_gen(query: str) -> str:
|
|
135
|
+
"""SQL generation agent with multi-step workflow and validation.
|
|
136
|
+
|
|
137
|
+
This example demonstrates:
|
|
138
|
+
- Dynamic system prompts with examples
|
|
139
|
+
- Output validation with ModelRetry
|
|
140
|
+
- Structured output with union types
|
|
141
|
+
- Agent dependencies for configuration
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
query: Natural language description of desired SQL query
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Generated SQL query or error message
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
deps = Deps(validate_sql=True)
|
|
151
|
+
result = await agent.run(query, deps=deps)
|
|
152
|
+
|
|
153
|
+
if isinstance(result.output, Success):
|
|
154
|
+
response = f"✅ SQL Query Generated:\n\n{result.output.sql_query}"
|
|
155
|
+
if result.output.explanation:
|
|
156
|
+
response += f"\n\n📝 Explanation:\n{result.output.explanation}"
|
|
157
|
+
return response
|
|
158
|
+
else:
|
|
159
|
+
return f"❌ Error: {result.output.error_message}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# For local testing
|
|
163
|
+
async def test():
|
|
164
|
+
"""Test function for local development."""
|
|
165
|
+
test_queries = [
|
|
166
|
+
"show me logs from yesterday with level 'error'",
|
|
167
|
+
"find all records where foobar is true",
|
|
168
|
+
"show me records from the last week",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for query in test_queries:
|
|
172
|
+
print(f"\n{'='*60}")
|
|
173
|
+
print(f"Query: {query}")
|
|
174
|
+
print("=" * 60)
|
|
175
|
+
result = await pydantic_ai_sql_gen(query)
|
|
176
|
+
print(result)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
import asyncio
|
|
181
|
+
|
|
182
|
+
asyncio.run(test())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Simple example of using Pydantic AI to construct a Pydantic model from a text input.
|
|
2
|
+
|
|
3
|
+
This example demonstrates:
|
|
4
|
+
- Using Pydantic AI with structured output
|
|
5
|
+
- Integration with Pixie SDK for observability
|
|
6
|
+
|
|
7
|
+
Run with:
|
|
8
|
+
poetry run pixie
|
|
9
|
+
|
|
10
|
+
Then query via GraphQL:
|
|
11
|
+
subscription {
|
|
12
|
+
run(name: "pydantic_model_example", inputData: "The windy city in the US of A.") {
|
|
13
|
+
runId
|
|
14
|
+
status
|
|
15
|
+
data
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from pydantic import BaseModel
|
|
22
|
+
from pydantic_ai import Agent
|
|
23
|
+
import pixie
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MyModel(BaseModel):
|
|
27
|
+
"""Model representing a city and country."""
|
|
28
|
+
|
|
29
|
+
city: str
|
|
30
|
+
country: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Create the agent
|
|
34
|
+
model = os.getenv("PYDANTIC_AI_MODEL", "openai:gpt-4o-mini")
|
|
35
|
+
agent = Agent(model, output_type=MyModel)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pixie.app
|
|
39
|
+
async def pydantic_ai_structured_output(query: str) -> MyModel:
|
|
40
|
+
"""Extract city and country information from a text query.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query: Natural language text describing a city
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
MyModel with extracted city and country
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Run the agent
|
|
50
|
+
agent_result = await agent.run(query)
|
|
51
|
+
|
|
52
|
+
# Return the structured output
|
|
53
|
+
return agent_result.output
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
# For testing locally
|
|
58
|
+
import asyncio
|
|
59
|
+
|
|
60
|
+
async def test():
|
|
61
|
+
output = await pydantic_ai_structured_output("The windy city in the US of A.")
|
|
62
|
+
print(output)
|
|
63
|
+
|
|
64
|
+
asyncio.run(test())
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pydantic_ai import Agent
|
|
2
|
+
|
|
3
|
+
import pixie
|
|
4
|
+
|
|
5
|
+
agent = Agent(
|
|
6
|
+
name="Simple chatbot",
|
|
7
|
+
instructions="You are a helpful assistant.",
|
|
8
|
+
model="gpt-4o-mini",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pixie.app
|
|
13
|
+
async def example_chatbot():
|
|
14
|
+
"""A simple chatbot using Pydantic-AI agent with GPT-4o-mini.
|
|
15
|
+
|
|
16
|
+
An OpenAI API key environment variable *(`OPENAI_API_KEY`)* is required to run this example.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
yield "How can I help you today?"
|
|
20
|
+
messages = []
|
|
21
|
+
while True:
|
|
22
|
+
user_msg = yield pixie.InputRequired(str)
|
|
23
|
+
response = await agent.run(user_msg, message_history=messages)
|
|
24
|
+
messages = response.all_messages()
|
|
25
|
+
yield response.output
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""An interactive agent that writes haikus while taking naps."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import logging
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic_ai import Agent, RunContext
|
|
8
|
+
from pydantic_ai.models.openai import OpenAIChatModelSettings
|
|
9
|
+
|
|
10
|
+
import pixie
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PoetDeps:
|
|
17
|
+
"""Dependencies for the sleepy poet agent."""
|
|
18
|
+
|
|
19
|
+
queue: asyncio.Queue
|
|
20
|
+
time_to_write: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
poet = Agent(
|
|
24
|
+
"openai:gpt-4o-mini",
|
|
25
|
+
system_prompt=(
|
|
26
|
+
"You are a sleepy poet. When given a topic, you first take a nap by calling the "
|
|
27
|
+
"`sleep_for_a_bit` tool once and once only, then write a haiku about the topic."
|
|
28
|
+
),
|
|
29
|
+
deps_type=PoetDeps,
|
|
30
|
+
model_settings=OpenAIChatModelSettings(
|
|
31
|
+
parallel_tool_calls=False # Ensure single sleep tool call per turn
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@poet.tool
|
|
37
|
+
async def sleep_for_a_bit(ctx: RunContext[PoetDeps]) -> str:
|
|
38
|
+
"""Sleep for 5 seconds before writing a haiku."""
|
|
39
|
+
if ctx.deps.time_to_write:
|
|
40
|
+
return "I've already napped, It's time to write the haiku now."
|
|
41
|
+
|
|
42
|
+
await ctx.deps.queue.put("Let me take a nap...")
|
|
43
|
+
for i in range(3):
|
|
44
|
+
await asyncio.sleep(1)
|
|
45
|
+
await ctx.deps.queue.put("z" * (i + 1))
|
|
46
|
+
ctx.deps.time_to_write = True
|
|
47
|
+
return "Poet napped for 3 seconds."
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HaikuRequest(BaseModel):
|
|
51
|
+
"""Input config for sleepy haiku agent."""
|
|
52
|
+
|
|
53
|
+
topic: str = Field(..., description="The topic to write haikus about.")
|
|
54
|
+
count: int = Field(3, description="Number of haikus to write.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pixie.app
|
|
58
|
+
async def example_sleepy_poet() -> pixie.PixieGenerator[str, HaikuRequest]:
|
|
59
|
+
"""An interactive agent that writes haikus while taking naps.
|
|
60
|
+
|
|
61
|
+
This agent accepts a topic and number of haikus to write, then for each haiku,
|
|
62
|
+
it first calls a tool to sleep for a few seconds before composing the haiku.
|
|
63
|
+
|
|
64
|
+
Yields:
|
|
65
|
+
HaikuRequest: Requirement for user's input for the haiku request.
|
|
66
|
+
str: The generated haikus interleaved with sleep updates.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
q = asyncio.Queue[str | None]()
|
|
70
|
+
deps = PoetDeps(queue=q)
|
|
71
|
+
|
|
72
|
+
yield "I'm the best poet in town! I can write haikus on any topic."
|
|
73
|
+
|
|
74
|
+
while True:
|
|
75
|
+
yield "What's your request?"
|
|
76
|
+
config = yield pixie.InputRequired(HaikuRequest)
|
|
77
|
+
|
|
78
|
+
async def write_haikus():
|
|
79
|
+
for i in range(0, config.count):
|
|
80
|
+
result = await poet.run(config.topic, deps=deps)
|
|
81
|
+
await q.put(f"### Haiku #{i+1}\n{result.output}\n")
|
|
82
|
+
deps.time_to_write = False # Reset for next haiku
|
|
83
|
+
|
|
84
|
+
await q.put(None) # Signal completion
|
|
85
|
+
|
|
86
|
+
task = asyncio.create_task(write_haikus())
|
|
87
|
+
try:
|
|
88
|
+
while True:
|
|
89
|
+
update = await q.get()
|
|
90
|
+
if update is None:
|
|
91
|
+
break
|
|
92
|
+
yield update
|
|
93
|
+
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
task.cancel()
|
|
96
|
+
return
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Interactive weather agent."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
from httpx import AsyncClient
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from pydantic_ai import Agent, RunContext
|
|
9
|
+
import pixie
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Deps:
|
|
14
|
+
"""Dependencies for the weather agent."""
|
|
15
|
+
|
|
16
|
+
client: AsyncClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LatLng(BaseModel):
|
|
20
|
+
"""Latitude and longitude coordinates."""
|
|
21
|
+
|
|
22
|
+
lat: float
|
|
23
|
+
lng: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Create the weather agent
|
|
27
|
+
agent = Agent(
|
|
28
|
+
"openai:gpt-4o-mini",
|
|
29
|
+
instructions="Be concise, reply with one sentence.",
|
|
30
|
+
deps_type=Deps,
|
|
31
|
+
retries=2,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@agent.tool
|
|
36
|
+
async def get_lat_lng(ctx: RunContext[Deps], location_description: str) -> LatLng:
|
|
37
|
+
"""Get the latitude and longitude of a location.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
ctx: The context.
|
|
41
|
+
location_description: A description of a location.
|
|
42
|
+
"""
|
|
43
|
+
# NOTE: Uses demo endpoints that return random data
|
|
44
|
+
r = await ctx.deps.client.get(
|
|
45
|
+
"https://demo-endpoints.pydantic.workers.dev/latlng",
|
|
46
|
+
params={"location": location_description},
|
|
47
|
+
)
|
|
48
|
+
r.raise_for_status()
|
|
49
|
+
return LatLng.model_validate_json(r.content)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@agent.tool
|
|
53
|
+
async def get_weather(ctx: RunContext[Deps], lat: float, lng: float) -> dict[str, Any]:
|
|
54
|
+
"""Get the weather at a location.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ctx: The context.
|
|
58
|
+
lat: Latitude of the location.
|
|
59
|
+
lng: Longitude of the location.
|
|
60
|
+
"""
|
|
61
|
+
# NOTE: Uses demo endpoints that return random data
|
|
62
|
+
temp_response, descr_response = await asyncio.gather(
|
|
63
|
+
ctx.deps.client.get(
|
|
64
|
+
"https://demo-endpoints.pydantic.workers.dev/number",
|
|
65
|
+
params={"min": 10, "max": 30},
|
|
66
|
+
),
|
|
67
|
+
ctx.deps.client.get(
|
|
68
|
+
"https://demo-endpoints.pydantic.workers.dev/weather",
|
|
69
|
+
params={"lat": lat, "lng": lng},
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
temp_response.raise_for_status()
|
|
73
|
+
descr_response.raise_for_status()
|
|
74
|
+
return {
|
|
75
|
+
"temperature": f"{temp_response.text} °C",
|
|
76
|
+
"description": descr_response.text,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pixie.app
|
|
81
|
+
async def example_weather_agent() -> pixie.PixieGenerator[str, str]:
|
|
82
|
+
"""Interactive weather agent.
|
|
83
|
+
|
|
84
|
+
This agent interacts with the user to get weather information for a specified location.
|
|
85
|
+
It uses tools to fetch latitude/longitude and weather data, and supports a multi-turn
|
|
86
|
+
conversation with the user.
|
|
87
|
+
|
|
88
|
+
The agent will:
|
|
89
|
+
- Prompt the user for a location.
|
|
90
|
+
- Fetch latitude and longitude for the location.
|
|
91
|
+
- Retrieve weather data for the coordinates.
|
|
92
|
+
- Display the weather information to the user.
|
|
93
|
+
|
|
94
|
+
The conversation continues until the user enters a stop word ('exit', 'quit', or 'stop').
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
yield "Hi! I can help you find the weather for any location."
|
|
98
|
+
yield "Enter 'exit', 'quit', or 'stop' to end the conversation."
|
|
99
|
+
|
|
100
|
+
# Create HTTP client and dependencies
|
|
101
|
+
async with AsyncClient() as client:
|
|
102
|
+
deps = Deps(client=client)
|
|
103
|
+
|
|
104
|
+
user_message = None
|
|
105
|
+
stop_words = ["exit", "quit", "stop"]
|
|
106
|
+
while user_message not in stop_words:
|
|
107
|
+
yield "What location would you like the weather for?"
|
|
108
|
+
user_message = yield pixie.InputRequired(str)
|
|
109
|
+
result = await agent.run(user_message, deps=deps)
|
|
110
|
+
yield result.output
|