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,246 @@
|
|
|
1
|
+
# PydanticAI Examples with Pixie SDK
|
|
2
|
+
|
|
3
|
+
This directory contains examples from the [PydanticAI Catalog](https://github.com/pydantic/pydantic-ai) integrated with Pixie SDK for observability and execution.
|
|
4
|
+
|
|
5
|
+
## Examples from Catalog
|
|
6
|
+
|
|
7
|
+
All examples are sourced from the official PydanticAI catalog and adapted for Pixie SDK.
|
|
8
|
+
|
|
9
|
+
### 1. pydantic_model.py - Pydantic Model (Quickstart)
|
|
10
|
+
|
|
11
|
+
Simple example demonstrating structured output extraction from text.
|
|
12
|
+
|
|
13
|
+
**Pixie Handler:** `pydantic_model_example`
|
|
14
|
+
**Documentation:** [pydantic-model](https://ai.pydantic.dev/examples/pydantic-model/)
|
|
15
|
+
**Source:** [pydantic_model.py](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/pydantic_model.py)
|
|
16
|
+
|
|
17
|
+
- **Pattern:** Async function
|
|
18
|
+
- **Input:** String query (e.g., "The windy city in the US of A.")
|
|
19
|
+
- **Output:** Structured `MyModel` with city and country
|
|
20
|
+
- **Features:**
|
|
21
|
+
- Basic agent setup with structured output
|
|
22
|
+
- Pydantic model validation
|
|
23
|
+
- Simple query → structured response
|
|
24
|
+
|
|
25
|
+
### 2. bank_support.py - Bank Support (Multi-Turn Chatbot)
|
|
26
|
+
|
|
27
|
+
Interactive bank support agent with tools and conversation history.
|
|
28
|
+
|
|
29
|
+
**Pixie Handler:** `bank_support_agent`
|
|
30
|
+
**Documentation:** [bank-support](https://ai.pydantic.dev/examples/bank-support/)
|
|
31
|
+
**Source:** [bank_support.py](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/bank_support.py)
|
|
32
|
+
|
|
33
|
+
- **Pattern:** Async generator (multi-turn)
|
|
34
|
+
- **Input:** None (interactive)
|
|
35
|
+
- **Output:** Generator[str, str] for conversation
|
|
36
|
+
- **Features:**
|
|
37
|
+
- Multi-turn conversation flow
|
|
38
|
+
- Tool usage (customer_balance)
|
|
39
|
+
- Dynamic system instructions
|
|
40
|
+
- SQLite database integration
|
|
41
|
+
- Interactive user input with `UserInputRequirement`
|
|
42
|
+
|
|
43
|
+
### 3. flight_booking.py - Flight Booking (Multi-Agent)
|
|
44
|
+
|
|
45
|
+
Multi-agent system for flight search, extraction, and seat selection.
|
|
46
|
+
|
|
47
|
+
**Pixie Handler:** `flight_booking_example`
|
|
48
|
+
**Documentation:** [flight-booking](https://ai.pydantic.dev/examples/flight-booking/)
|
|
49
|
+
**Source:** [flight_booking.py](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/flight_booking.py)
|
|
50
|
+
|
|
51
|
+
- **Pattern:** Async generator (interactive)
|
|
52
|
+
- **Input:** None (interactive)
|
|
53
|
+
- **Output:** Generator[str, str] for booking flow
|
|
54
|
+
- **Features:**
|
|
55
|
+
- **Agent Delegation:** search_agent delegates to extraction_agent
|
|
56
|
+
- **Programmatic Hand-off:** flight search → seat selection
|
|
57
|
+
- Multi-agent collaboration
|
|
58
|
+
- Usage limits and tracking
|
|
59
|
+
- Output validation with constraints
|
|
60
|
+
- Interactive confirmation workflow
|
|
61
|
+
|
|
62
|
+
### 4. question_graph.py - Question Graph (Graph/State-Machine)
|
|
63
|
+
|
|
64
|
+
Graph-based Q&A system using pydantic_graph for state machine control flow.
|
|
65
|
+
|
|
66
|
+
**Pixie Handler:** `question_graph_example`
|
|
67
|
+
**Documentation:** [question-graph](https://ai.pydantic.dev/examples/question-graph/)
|
|
68
|
+
**Source:** [question_graph.py](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/question_graph.py)
|
|
69
|
+
|
|
70
|
+
- **Pattern:** Async generator (interactive with graph)
|
|
71
|
+
- **Input:** None (interactive)
|
|
72
|
+
- **Output:** Generator[str, str] for Q&A flow
|
|
73
|
+
- **Features:**
|
|
74
|
+
- **pydantic_graph** for state machine control flow
|
|
75
|
+
- Graph nodes: Ask → Answer → Evaluate → Reprimand/End
|
|
76
|
+
- State management across nodes
|
|
77
|
+
- Conditional branching (correct/incorrect)
|
|
78
|
+
- Message history per agent
|
|
79
|
+
|
|
80
|
+
**Graph Flow:**
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Ask (generate question)
|
|
84
|
+
↓
|
|
85
|
+
Answer (get user input)
|
|
86
|
+
↓
|
|
87
|
+
Evaluate (check answer)
|
|
88
|
+
↓
|
|
89
|
+
├─ Correct → End (success)
|
|
90
|
+
└─ Wrong → Reprimand → Ask (retry)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 5. sql_gen.py - SQL Generation (Multi-Step Workflow)
|
|
94
|
+
|
|
95
|
+
SQL query generation with validation and structured output.
|
|
96
|
+
|
|
97
|
+
**Pixie Handler:** `sql_gen_example`
|
|
98
|
+
**Documentation:** [sql-gen](https://ai.pydantic.dev/examples/sql-gen/)
|
|
99
|
+
**Source:** [sql_gen.py](https://github.com/pydantic/pydantic-ai/blob/main/examples/pydantic_ai_examples/sql_gen.py)
|
|
100
|
+
|
|
101
|
+
- **Pattern:** Async function
|
|
102
|
+
- **Input:** String query (natural language)
|
|
103
|
+
- **Output:** String (SQL query or error message)
|
|
104
|
+
- **Features:**
|
|
105
|
+
- Dynamic system prompts with SQL examples
|
|
106
|
+
- Output validation with `ModelRetry`
|
|
107
|
+
- Structured output with union types (`Success | InvalidRequest`)
|
|
108
|
+
- Multi-step validation workflow
|
|
109
|
+
- SQL safety checks (prevents dangerous operations)
|
|
110
|
+
- Schema-aware query generation
|
|
111
|
+
|
|
112
|
+
## Setup
|
|
113
|
+
|
|
114
|
+
### 1. Install Dependencies
|
|
115
|
+
|
|
116
|
+
From the project root:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
poetry install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 2. Configure Environment Variables
|
|
123
|
+
|
|
124
|
+
Copy `.env.example` to `.env` and add your API keys:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cp examples/pydantic_ai/.env.example examples/pydantic_ai/.env
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Edit `.env` and add:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Required for most examples
|
|
134
|
+
OPENAI_API_KEY=sk-...
|
|
135
|
+
|
|
136
|
+
# Optional
|
|
137
|
+
LOGFIRE_TOKEN=... # For observability
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3. Start Pixie Server
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
poetry run pixie
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The server will start at http://127.0.0.1:8000
|
|
147
|
+
|
|
148
|
+
## Running Examples
|
|
149
|
+
|
|
150
|
+
### Via GraphQL Playground
|
|
151
|
+
|
|
152
|
+
Open http://127.0.0.1:8000/graphql and run subscriptions:
|
|
153
|
+
|
|
154
|
+
#### Example 1: Pydantic Model
|
|
155
|
+
|
|
156
|
+
```graphql
|
|
157
|
+
subscription {
|
|
158
|
+
run(name: "pydantic_model_example", inputData: "The capital of France") {
|
|
159
|
+
runId
|
|
160
|
+
status
|
|
161
|
+
data
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Example 2: Bank Support (Interactive)
|
|
167
|
+
|
|
168
|
+
```graphql
|
|
169
|
+
subscription {
|
|
170
|
+
run(name: "bank_support_agent") {
|
|
171
|
+
runId
|
|
172
|
+
status
|
|
173
|
+
data
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Example 3: Flight Booking
|
|
179
|
+
|
|
180
|
+
```graphql
|
|
181
|
+
subscription {
|
|
182
|
+
run(name: "flight_booking_example") {
|
|
183
|
+
runId
|
|
184
|
+
status
|
|
185
|
+
data
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Example 4: Question Graph
|
|
191
|
+
|
|
192
|
+
```graphql
|
|
193
|
+
subscription {
|
|
194
|
+
run(name: "question_graph_example") {
|
|
195
|
+
runId
|
|
196
|
+
status
|
|
197
|
+
data
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Example 5: SQL Generation
|
|
203
|
+
|
|
204
|
+
```graphql
|
|
205
|
+
subscription {
|
|
206
|
+
run(name: "sql_gen_example", inputData: "show me error logs from yesterday") {
|
|
207
|
+
runId
|
|
208
|
+
status
|
|
209
|
+
data
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Architecture Patterns
|
|
215
|
+
|
|
216
|
+
### Async Functions vs Generators
|
|
217
|
+
|
|
218
|
+
- **Async Function** (`async def → Type`): Single request-response (pydantic_model, sql_gen)
|
|
219
|
+
- **Async Generator** (`async def → PixieGenerator[YieldType, SendType]`): Multi-turn (bank_support, flight_booking, question_graph)
|
|
220
|
+
|
|
221
|
+
### Agent Patterns
|
|
222
|
+
|
|
223
|
+
1. **Single Agent** - One agent handles workflow
|
|
224
|
+
2. **Multi-Turn Chat** - Agent + user interaction loop
|
|
225
|
+
3. **Agent Delegation** - One agent calls another
|
|
226
|
+
4. **Programmatic Hand-off** - Sequential agent execution
|
|
227
|
+
5. **Graph-Based Control** - pydantic_graph state machines
|
|
228
|
+
|
|
229
|
+
## Integration with Pixie SDK
|
|
230
|
+
|
|
231
|
+
All examples use:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from pixie import app, PixieGenerator, UserInputRequirement
|
|
235
|
+
|
|
236
|
+
@app
|
|
237
|
+
async def my_handler(input: InputType) -> OutputType:
|
|
238
|
+
Agent.instrument_all() # Enable observability
|
|
239
|
+
# ... implementation
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Resources
|
|
243
|
+
|
|
244
|
+
- [PydanticAI Documentation](https://ai.pydantic.dev/)
|
|
245
|
+
- [Pixie SDK Guide](../../.github/copilot-instructions.md)
|
|
246
|
+
- [PydanticAI GitHub](https://github.com/pydantic/pydantic-ai)
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Bank support agent example using Pydantic AI.
|
|
2
|
+
|
|
3
|
+
This example demonstrates:
|
|
4
|
+
- Dynamic system prompts with agent instructions
|
|
5
|
+
- Structured output types
|
|
6
|
+
- Tools for querying dependencies
|
|
7
|
+
- Integration with Pixie SDK
|
|
8
|
+
|
|
9
|
+
Run with:
|
|
10
|
+
poetry run pixie
|
|
11
|
+
|
|
12
|
+
Then query via GraphQL:
|
|
13
|
+
subscription {
|
|
14
|
+
run(name: "bank_support_agent", inputData: "What is my balance?") {
|
|
15
|
+
runId
|
|
16
|
+
status
|
|
17
|
+
data
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sqlite3
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pydantic import BaseModel
|
|
25
|
+
from pydantic_ai import Agent, RunContext
|
|
26
|
+
import pixie
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DatabaseConn:
|
|
31
|
+
"""A wrapper over the SQLite connection."""
|
|
32
|
+
|
|
33
|
+
sqlite_conn: sqlite3.Connection
|
|
34
|
+
|
|
35
|
+
async def customer_name(self, *, customer_id: int) -> str | None:
|
|
36
|
+
res = self.sqlite_conn.execute(
|
|
37
|
+
"SELECT name FROM customers WHERE id=?", (customer_id,)
|
|
38
|
+
)
|
|
39
|
+
row = res.fetchone()
|
|
40
|
+
if row:
|
|
41
|
+
return row[0]
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
async def customer_balance(self, *, customer_id: int) -> float:
|
|
45
|
+
res = self.sqlite_conn.execute(
|
|
46
|
+
"SELECT balance FROM customers WHERE id=?", (customer_id,)
|
|
47
|
+
)
|
|
48
|
+
row = res.fetchone()
|
|
49
|
+
if row:
|
|
50
|
+
return row[0]
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError("Customer not found")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class SupportDependencies:
|
|
57
|
+
customer_id: int
|
|
58
|
+
db: DatabaseConn
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SupportOutput(BaseModel):
|
|
62
|
+
"""Output model for bank support agent."""
|
|
63
|
+
|
|
64
|
+
support_advice: str
|
|
65
|
+
"""Advice returned to the customer"""
|
|
66
|
+
block_card: bool
|
|
67
|
+
"""Whether to block their card or not"""
|
|
68
|
+
risk: int
|
|
69
|
+
"""Risk level of query"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Create the support agent
|
|
73
|
+
support_agent = Agent(
|
|
74
|
+
"openai:gpt-4o-mini",
|
|
75
|
+
deps_type=SupportDependencies,
|
|
76
|
+
output_type=SupportOutput,
|
|
77
|
+
instructions=(
|
|
78
|
+
"You are a support agent in our bank, give the "
|
|
79
|
+
"customer support and judge the risk level of their query. "
|
|
80
|
+
"Reply using the customer's name."
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@support_agent.instructions
|
|
86
|
+
async def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:
|
|
87
|
+
customer_name = await ctx.deps.db.customer_name(customer_id=ctx.deps.customer_id)
|
|
88
|
+
return f"The customer's name is {customer_name!r}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@support_agent.tool
|
|
92
|
+
async def customer_balance_tool(ctx: RunContext[SupportDependencies]) -> str:
|
|
93
|
+
"""Returns the customer's current account balance."""
|
|
94
|
+
balance = await ctx.deps.db.customer_balance(
|
|
95
|
+
customer_id=ctx.deps.customer_id,
|
|
96
|
+
)
|
|
97
|
+
return f"${balance:.2f}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pixie.app
|
|
101
|
+
async def pydantic_ai_bank_support_agent() -> pixie.PixieGenerator[str, str]:
|
|
102
|
+
"""Interactive bank support agent.
|
|
103
|
+
|
|
104
|
+
This agent helps customers with banking queries, can check balances,
|
|
105
|
+
and assesses risk levels of queries.
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
str: Agent responses and support advice
|
|
109
|
+
|
|
110
|
+
Receives:
|
|
111
|
+
str: User queries via InputRequired
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# Initialize database connection (in-memory for demo)
|
|
115
|
+
with sqlite3.connect(":memory:") as con:
|
|
116
|
+
cur = con.cursor()
|
|
117
|
+
cur.execute("CREATE TABLE customers(id, name, balance)")
|
|
118
|
+
cur.execute(
|
|
119
|
+
"""
|
|
120
|
+
INSERT INTO customers VALUES
|
|
121
|
+
(123, 'John', 123.45),
|
|
122
|
+
(456, 'Jane', 567.89)
|
|
123
|
+
"""
|
|
124
|
+
)
|
|
125
|
+
con.commit()
|
|
126
|
+
|
|
127
|
+
# Default customer
|
|
128
|
+
customer_id = 123
|
|
129
|
+
deps = SupportDependencies(
|
|
130
|
+
customer_id=customer_id, db=DatabaseConn(sqlite_conn=con)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
yield "Welcome to Bank Support! I'm here to help you with your account."
|
|
134
|
+
|
|
135
|
+
while True:
|
|
136
|
+
# Get user query
|
|
137
|
+
user_query = yield pixie.InputRequired(str)
|
|
138
|
+
|
|
139
|
+
# Check for exit commands
|
|
140
|
+
if user_query.lower() in {"exit", "quit", "bye"}:
|
|
141
|
+
yield "Thank you for using our support service. Have a great day!"
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Run the agent
|
|
145
|
+
result = await support_agent.run(user_query, deps=deps)
|
|
146
|
+
|
|
147
|
+
# Format the response
|
|
148
|
+
output = result.output
|
|
149
|
+
response = f"""
|
|
150
|
+
Support Advice: {output.support_advice}
|
|
151
|
+
Card Status: {"🔒 BLOCKED" if output.block_card else "✓ Active"}
|
|
152
|
+
Risk Level: {output.risk}/10
|
|
153
|
+
"""
|
|
154
|
+
yield response.strip()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Example of a multi-agent flow where one agent delegates work to another.
|
|
2
|
+
|
|
3
|
+
This demonstrates agent delegation and programmatic agent hand-off patterns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from pydantic_ai import (
|
|
12
|
+
Agent,
|
|
13
|
+
ModelMessage,
|
|
14
|
+
ModelRetry,
|
|
15
|
+
RunContext,
|
|
16
|
+
RunUsage,
|
|
17
|
+
UsageLimits,
|
|
18
|
+
)
|
|
19
|
+
import pixie
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FlightDetails(BaseModel):
|
|
23
|
+
"""Details of the most suitable flight."""
|
|
24
|
+
|
|
25
|
+
flight_number: str
|
|
26
|
+
price: int
|
|
27
|
+
origin: str = Field(description="Three-letter airport code")
|
|
28
|
+
destination: str = Field(description="Three-letter airport code")
|
|
29
|
+
date: datetime.date
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NoFlightFound(BaseModel):
|
|
33
|
+
"""When no valid flight is found."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Deps:
|
|
38
|
+
web_page_text: str
|
|
39
|
+
req_origin: str
|
|
40
|
+
req_destination: str
|
|
41
|
+
req_date: datetime.date
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# This agent is responsible for controlling the flow of the conversation.
|
|
45
|
+
search_agent = Agent[Deps, FlightDetails | NoFlightFound](
|
|
46
|
+
"openai:gpt-4o-mini",
|
|
47
|
+
output_type=FlightDetails | NoFlightFound, # type: ignore
|
|
48
|
+
retries=4,
|
|
49
|
+
system_prompt=(
|
|
50
|
+
"Your job is to find the cheapest flight for the user on the given date. "
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# This agent is responsible for extracting flight details from web page text.
|
|
56
|
+
extraction_agent = Agent(
|
|
57
|
+
"openai:gpt-4o-mini",
|
|
58
|
+
output_type=list[FlightDetails],
|
|
59
|
+
system_prompt="Extract all the flight details from the given text.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@search_agent.tool
|
|
64
|
+
async def extract_flights(ctx: RunContext[Deps]) -> list[FlightDetails]:
|
|
65
|
+
"""Get details of all flights."""
|
|
66
|
+
# we pass the usage to the search agent so requests within this agent are counted
|
|
67
|
+
agent_result = await extraction_agent.run(ctx.deps.web_page_text, usage=ctx.usage)
|
|
68
|
+
return agent_result.output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@search_agent.output_validator
|
|
72
|
+
async def validate_output(
|
|
73
|
+
ctx: RunContext[Deps], output: FlightDetails | NoFlightFound
|
|
74
|
+
) -> FlightDetails | NoFlightFound:
|
|
75
|
+
"""Procedural validation that the flight meets the constraints."""
|
|
76
|
+
if isinstance(output, NoFlightFound):
|
|
77
|
+
return output
|
|
78
|
+
|
|
79
|
+
errors: list[str] = []
|
|
80
|
+
if output.origin != ctx.deps.req_origin:
|
|
81
|
+
errors.append(
|
|
82
|
+
f"Flight should have origin {ctx.deps.req_origin}, not {output.origin}"
|
|
83
|
+
)
|
|
84
|
+
if output.destination != ctx.deps.req_destination:
|
|
85
|
+
errors.append(
|
|
86
|
+
f"Flight should have destination {ctx.deps.req_destination}, not {output.destination}"
|
|
87
|
+
)
|
|
88
|
+
if output.date != ctx.deps.req_date:
|
|
89
|
+
errors.append(f"Flight should be on {ctx.deps.req_date}, not {output.date}")
|
|
90
|
+
|
|
91
|
+
if errors:
|
|
92
|
+
raise ModelRetry("\n".join(errors))
|
|
93
|
+
else:
|
|
94
|
+
return output
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SeatPreference(BaseModel):
|
|
98
|
+
row: int = Field(ge=1, le=30)
|
|
99
|
+
seat: Literal["A", "B", "C", "D", "E", "F"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Failed(BaseModel):
|
|
103
|
+
"""Unable to extract a seat selection."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# This agent is responsible for extracting the user's seat selection
|
|
107
|
+
seat_preference_agent = Agent[None, SeatPreference | Failed](
|
|
108
|
+
"openai:gpt-4o-mini",
|
|
109
|
+
output_type=SeatPreference | Failed,
|
|
110
|
+
system_prompt=(
|
|
111
|
+
"Extract the user's seat preference. "
|
|
112
|
+
"Seats A and F are window seats. "
|
|
113
|
+
"Row 1 is the front row and has extra leg room. "
|
|
114
|
+
"Rows 14, and 20 also have extra leg room. "
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# in reality this would be downloaded from a booking site,
|
|
120
|
+
# potentially using another agent to navigate the site
|
|
121
|
+
flights_web_page = """
|
|
122
|
+
1. Flight SFO-AK123
|
|
123
|
+
- Price: $350
|
|
124
|
+
- Origin: San Francisco International Airport (SFO)
|
|
125
|
+
- Destination: Ted Stevens Anchorage International Airport (ANC)
|
|
126
|
+
- Date: January 10, 2025
|
|
127
|
+
|
|
128
|
+
2. Flight SFO-AK456
|
|
129
|
+
- Price: $370
|
|
130
|
+
- Origin: San Francisco International Airport (SFO)
|
|
131
|
+
- Destination: Fairbanks International Airport (FAI)
|
|
132
|
+
- Date: January 10, 2025
|
|
133
|
+
|
|
134
|
+
3. Flight SFO-AK789
|
|
135
|
+
- Price: $400
|
|
136
|
+
- Origin: San Francisco International Airport (SFO)
|
|
137
|
+
- Destination: Juneau International Airport (JNU)
|
|
138
|
+
- Date: January 20, 2025
|
|
139
|
+
|
|
140
|
+
4. Flight NYC-LA101
|
|
141
|
+
- Price: $250
|
|
142
|
+
- Origin: San Francisco International Airport (SFO)
|
|
143
|
+
- Destination: Ted Stevens Anchorage International Airport (ANC)
|
|
144
|
+
- Date: January 10, 2025
|
|
145
|
+
|
|
146
|
+
5. Flight CHI-MIA202
|
|
147
|
+
- Price: $200
|
|
148
|
+
- Origin: Chicago O'Hare International Airport (ORD)
|
|
149
|
+
- Destination: Miami International Airport (MIA)
|
|
150
|
+
- Date: January 12, 2025
|
|
151
|
+
|
|
152
|
+
6. Flight BOS-SEA303
|
|
153
|
+
- Price: $120
|
|
154
|
+
- Origin: Boston Logan International Airport (BOS)
|
|
155
|
+
- Destination: Ted Stevens Anchorage International Airport (ANC)
|
|
156
|
+
- Date: January 12, 2025
|
|
157
|
+
|
|
158
|
+
7. Flight DFW-DEN404
|
|
159
|
+
- Price: $150
|
|
160
|
+
- Origin: Dallas/Fort Worth International Airport (DFW)
|
|
161
|
+
- Destination: Denver International Airport (DEN)
|
|
162
|
+
- Date: January 10, 2025
|
|
163
|
+
|
|
164
|
+
8. Flight ATL-HOU505
|
|
165
|
+
- Price: $180
|
|
166
|
+
- Origin: Hartsfield-Jackson Atlanta International Airport (ATL)
|
|
167
|
+
- Destination: George Bush Intercontinental Airport (IAH)
|
|
168
|
+
- Date: January 10, 2025
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
# restrict how many requests this app can make to the LLM
|
|
172
|
+
usage_limits = UsageLimits(request_limit=15)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pixie.app
|
|
176
|
+
async def pydantic_ai_flight_booking() -> pixie.PixieGenerator[str, str]:
|
|
177
|
+
"""Multi-agent flight booking with search, extraction, and seat selection.
|
|
178
|
+
|
|
179
|
+
This example demonstrates:
|
|
180
|
+
- Agent delegation (search_agent -> extraction_agent)
|
|
181
|
+
- Programmatic agent hand-off (search -> seat selection)
|
|
182
|
+
- Interactive user input for flight confirmation and seat preference
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
deps = Deps(
|
|
186
|
+
web_page_text=flights_web_page,
|
|
187
|
+
req_origin="SFO",
|
|
188
|
+
req_destination="ANC",
|
|
189
|
+
req_date=datetime.date(2025, 1, 10),
|
|
190
|
+
)
|
|
191
|
+
message_history: list[ModelMessage] | None = None
|
|
192
|
+
usage: RunUsage = RunUsage()
|
|
193
|
+
|
|
194
|
+
yield "🛫 Welcome to Flight Booking! Searching for flights..."
|
|
195
|
+
|
|
196
|
+
# Flight search loop
|
|
197
|
+
while True:
|
|
198
|
+
agent_result = await search_agent.run(
|
|
199
|
+
f"Find me a flight from {deps.req_origin} to {deps.req_destination} on {deps.req_date}",
|
|
200
|
+
deps=deps,
|
|
201
|
+
usage=usage,
|
|
202
|
+
message_history=message_history,
|
|
203
|
+
usage_limits=usage_limits,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if isinstance(agent_result.output, NoFlightFound):
|
|
207
|
+
yield "❌ No flight found. Ending search."
|
|
208
|
+
break
|
|
209
|
+
else:
|
|
210
|
+
flight = agent_result.output
|
|
211
|
+
yield f"✈️ Flight found: {flight.flight_number}"
|
|
212
|
+
yield f" Price: ${flight.price}"
|
|
213
|
+
yield f" Route: {flight.origin} → {flight.destination}"
|
|
214
|
+
yield f" Date: {flight.date}"
|
|
215
|
+
yield "\nDo you want to buy this flight, or keep searching? (buy/search)"
|
|
216
|
+
|
|
217
|
+
answer = yield pixie.InputRequired(str)
|
|
218
|
+
answer = answer.lower().strip()
|
|
219
|
+
|
|
220
|
+
if answer == "buy":
|
|
221
|
+
# Move to seat selection
|
|
222
|
+
yield "\n🪑 Great! Now let's select your seat..."
|
|
223
|
+
seat_message_history: list[ModelMessage] | None = None
|
|
224
|
+
|
|
225
|
+
while True:
|
|
226
|
+
yield 'What seat would you like? (e.g., "row 1 seat A" or "window seat with leg room")'
|
|
227
|
+
seat_input = yield pixie.InputRequired(str)
|
|
228
|
+
|
|
229
|
+
seat_result = await seat_preference_agent.run(
|
|
230
|
+
seat_input,
|
|
231
|
+
message_history=seat_message_history,
|
|
232
|
+
usage=usage,
|
|
233
|
+
usage_limits=usage_limits,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if isinstance(seat_result.output, SeatPreference):
|
|
237
|
+
seat = seat_result.output
|
|
238
|
+
yield "\n✅ Booking confirmed!"
|
|
239
|
+
yield f" Flight: {flight.flight_number}"
|
|
240
|
+
yield f" Seat: Row {seat.row}, Seat {seat.seat}"
|
|
241
|
+
yield f" Total Cost: ${flight.price}"
|
|
242
|
+
return
|
|
243
|
+
else:
|
|
244
|
+
yield "❌ Could not understand seat preference. Please try again."
|
|
245
|
+
seat_message_history = seat_result.all_messages()
|
|
246
|
+
else:
|
|
247
|
+
yield "\n🔄 Searching for another flight..."
|
|
248
|
+
message_history = agent_result.all_messages(
|
|
249
|
+
output_tool_return_content="Please suggest another flight"
|
|
250
|
+
)
|