chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Comprehensive example demonstrating AdvancedSQLiteSession functionality.
|
||||
|
||||
This example shows both basic session memory features and advanced conversation
|
||||
branching capabilities, including usage statistics, turn-based organization,
|
||||
and multi-timeline conversation management.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
from agents.extensions.memory import AdvancedSQLiteSession
|
||||
|
||||
|
||||
@function_tool
|
||||
async def get_weather(city: str) -> str:
|
||||
if city.strip().lower() == "new york":
|
||||
return f"The weather in {city} is cloudy."
|
||||
return f"The weather in {city} is sunny."
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Create an advanced session instance
|
||||
session = AdvancedSQLiteSession(
|
||||
session_id="conversation_comprehensive",
|
||||
create_tables=True,
|
||||
)
|
||||
|
||||
print("=== AdvancedSQLiteSession Comprehensive Example ===")
|
||||
print("This example demonstrates both basic and advanced session features.\n")
|
||||
|
||||
# === PART 1: Basic Session Functionality ===
|
||||
print("=== PART 1: Basic Session Memory ===")
|
||||
print("The agent will remember previous messages with structured tracking.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print(f"Usage: {result.context_wrapper.usage.total_tokens} tokens")
|
||||
|
||||
# Store usage data automatically
|
||||
await session.store_run_usage(result)
|
||||
print()
|
||||
|
||||
# Second turn - continuing the conversation
|
||||
print("Second turn:")
|
||||
print("User: What's the weather in that city?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the weather in that city?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print(f"Usage: {result.context_wrapper.usage.total_tokens} tokens")
|
||||
|
||||
# Store usage data automatically
|
||||
await session.store_run_usage(result)
|
||||
print()
|
||||
|
||||
# Third turn
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that city?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that city?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print(f"Usage: {result.context_wrapper.usage.total_tokens} tokens")
|
||||
|
||||
# Store usage data automatically
|
||||
await session.store_run_usage(result)
|
||||
print()
|
||||
|
||||
# === PART 2: Usage Tracking and Analytics ===
|
||||
print("=== PART 2: Usage Tracking and Analytics ===")
|
||||
session_usage = await session.get_session_usage()
|
||||
if session_usage:
|
||||
print("Session Usage (aggregated from turns):")
|
||||
print(f" Total requests: {session_usage['requests']}")
|
||||
print(f" Total tokens: {session_usage['total_tokens']}")
|
||||
print(f" Input tokens: {session_usage['input_tokens']}")
|
||||
print(f" Output tokens: {session_usage['output_tokens']}")
|
||||
print(f" Total turns: {session_usage['total_turns']}")
|
||||
|
||||
# Show usage by turn
|
||||
turn_usage_list = await session.get_turn_usage()
|
||||
if turn_usage_list and isinstance(turn_usage_list, list):
|
||||
print("\nUsage by turn:")
|
||||
for turn_data in turn_usage_list:
|
||||
turn_num = turn_data["user_turn_number"]
|
||||
tokens = turn_data["total_tokens"]
|
||||
print(f" Turn {turn_num}: {tokens} tokens")
|
||||
else:
|
||||
print("No usage data found.")
|
||||
|
||||
print("\n=== Structured Query Demo ===")
|
||||
conversation_turns = await session.get_conversation_by_turns()
|
||||
print("Conversation by turns:")
|
||||
for turn_num, items in conversation_turns.items():
|
||||
print(f" Turn {turn_num}: {len(items)} items")
|
||||
for item in items:
|
||||
if item["tool_name"]:
|
||||
print(f" - {item['type']} (tool: {item['tool_name']})")
|
||||
else:
|
||||
print(f" - {item['type']}")
|
||||
|
||||
# Show tool usage
|
||||
tool_usage = await session.get_tool_usage()
|
||||
if tool_usage:
|
||||
print("\nTool usage:")
|
||||
for tool_name, count, turn in tool_usage:
|
||||
print(f" {tool_name}: used {count} times in turn {turn}")
|
||||
else:
|
||||
print("\nNo tool usage found.")
|
||||
|
||||
print("\n=== Original Conversation Complete ===")
|
||||
|
||||
# Show current conversation
|
||||
print("Current conversation:")
|
||||
current_items = await session.get_items()
|
||||
for i, item in enumerate(current_items, 1): # type: ignore[assignment]
|
||||
role = str(item.get("role", item.get("type", "unknown")))
|
||||
if item.get("type") == "function_call":
|
||||
content = f"{item.get('name', 'unknown')}({item.get('arguments', '{}')})"
|
||||
elif item.get("type") == "function_call_output":
|
||||
content = str(item.get("output", ""))
|
||||
else:
|
||||
content = str(item.get("content", item.get("output", "")))
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nTotal items: {len(current_items)}")
|
||||
|
||||
# === PART 3: Conversation Branching ===
|
||||
print("\n=== PART 3: Conversation Branching ===")
|
||||
print("Let's explore a different path starting before turn 2...")
|
||||
|
||||
# Show available turns for branching
|
||||
print("\nAvailable turns for branching:")
|
||||
turns = await session.get_conversation_turns()
|
||||
for turn in turns: # type: ignore[assignment]
|
||||
print(f" Turn {turn['turn']}: {turn['content']}") # type: ignore[index]
|
||||
|
||||
# Create a branch from turn 2
|
||||
print("\nCreating new branch from turn 2...")
|
||||
branch_id = await session.create_branch_from_turn(2)
|
||||
print(f"Created branch: {branch_id}")
|
||||
|
||||
# Show what's in the new branch (it should contain items created before turn 2)
|
||||
branch_items = await session.get_items()
|
||||
print(f"Items copied to new branch: {len(branch_items)}")
|
||||
print("New branch starts before turn 2 and contains:")
|
||||
for i, item in enumerate(branch_items, 1): # type: ignore[assignment]
|
||||
role = str(item.get("role", item.get("type", "unknown")))
|
||||
if item.get("type") == "function_call":
|
||||
content = f"{item.get('name', 'unknown')}({item.get('arguments', '{}')})"
|
||||
elif item.get("type") == "function_call_output":
|
||||
content = str(item.get("output", ""))
|
||||
else:
|
||||
content = str(item.get("content", item.get("output", "")))
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
# Continue conversation in new branch
|
||||
print("\nContinuing conversation in new branch...")
|
||||
print("Turn 2 (new branch): User asks about New York instead")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Actually, what's the weather in New York instead?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
await session.store_run_usage(result)
|
||||
|
||||
# Continue the new branch
|
||||
print("Turn 3 (new branch): User asks about NYC attractions")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What are some famous attractions in New York?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
await session.store_run_usage(result)
|
||||
|
||||
# Show the new conversation
|
||||
print("\n=== New Conversation Branch ===")
|
||||
new_conversation = await session.get_items()
|
||||
print("New conversation with branch:")
|
||||
for i, item in enumerate(new_conversation, 1): # type: ignore[assignment]
|
||||
role = str(item.get("role", item.get("type", "unknown")))
|
||||
if item.get("type") == "function_call":
|
||||
content = f"{item.get('name', 'unknown')}({item.get('arguments', '{}')})"
|
||||
elif item.get("type") == "function_call_output":
|
||||
content = str(item.get("output", ""))
|
||||
else:
|
||||
content = str(item.get("content", item.get("output", "")))
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nTotal items in new branch: {len(new_conversation)}")
|
||||
|
||||
# === PART 4: Branch Management ===
|
||||
print("\n=== PART 4: Branch Management ===")
|
||||
# Show all branches
|
||||
branches = await session.list_branches()
|
||||
print("All branches in this session:")
|
||||
for branch in branches:
|
||||
current = " (current)" if branch["is_current"] else ""
|
||||
print(
|
||||
f" {branch['branch_id']}: {branch['user_turns']} user turns, {branch['message_count']} total messages{current}"
|
||||
)
|
||||
|
||||
# Show conversation turns in current branch
|
||||
print("\nConversation turns in current branch:")
|
||||
current_turns = await session.get_conversation_turns()
|
||||
for turn in current_turns: # type: ignore[assignment]
|
||||
print(f" Turn {turn['turn']}: {turn['content']}") # type: ignore[index]
|
||||
|
||||
print("\n=== Branch Switching Demo ===")
|
||||
print("We can switch back to the main branch...")
|
||||
|
||||
# Switch back to main branch
|
||||
await session.switch_to_branch("main")
|
||||
print("Switched to main branch")
|
||||
|
||||
# Show what's in main branch
|
||||
main_items = await session.get_items()
|
||||
print(f"Items in main branch: {len(main_items)}")
|
||||
|
||||
# Switch back to new branch
|
||||
await session.switch_to_branch(branch_id)
|
||||
branch_items = await session.get_items()
|
||||
print(f"Items in new branch: {len(branch_items)}")
|
||||
|
||||
print("\n=== Final Summary ===")
|
||||
await session.switch_to_branch("main")
|
||||
main_final = len(await session.get_items())
|
||||
await session.switch_to_branch(branch_id)
|
||||
branch_final = len(await session.get_items())
|
||||
|
||||
print(f"Main branch items: {main_final}")
|
||||
print(f"New branch items: {branch_final}")
|
||||
|
||||
# Show that branches are completely independent
|
||||
print("\nBranches are completely independent:")
|
||||
print("- Main branch has full original conversation")
|
||||
print("- New branch has turn 1 + new conversation path")
|
||||
print("- No interference between branches!")
|
||||
|
||||
print("\n=== Comprehensive Example Complete ===")
|
||||
print("This demonstrates the full AdvancedSQLiteSession capabilities!")
|
||||
print("Key features:")
|
||||
print("- Structured conversation tracking with usage analytics")
|
||||
print("- Turn-based organization and querying")
|
||||
print("- Create branches from any user message")
|
||||
print("- Branches inherit conversation history up to the branch point")
|
||||
print("- Complete branch isolation - no interference between branches")
|
||||
print("- Easy branch switching and management")
|
||||
print("- No complex soft deletion - clean branch-based architecture")
|
||||
print("- Perfect for building AI systems with conversation editing capabilities!")
|
||||
|
||||
# Cleanup
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Example demonstrating OpenAI responses.compact session functionality.
|
||||
|
||||
This example shows how to use OpenAIResponsesCompactionSession to automatically
|
||||
compact conversation history when it grows too large, reducing token usage
|
||||
while preserving context.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, OpenAIResponsesCompactionSession, Runner, SQLiteSession
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an underlying session for storage
|
||||
underlying = SQLiteSession(":memory:")
|
||||
|
||||
# Wrap with compaction session - will automatically compact when threshold hit
|
||||
session = OpenAIResponsesCompactionSession(
|
||||
session_id="demo-session",
|
||||
underlying_session=underlying,
|
||||
model="gpt-4.1",
|
||||
# Custom compaction trigger (default is 10 candidates)
|
||||
should_trigger_compaction=lambda ctx: len(ctx["compaction_candidate_items"]) >= 4,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply concisely. Keep answers to 1-2 sentences.",
|
||||
)
|
||||
|
||||
print("=== Compaction Session Example ===\n")
|
||||
|
||||
prompts = [
|
||||
"What is the tallest mountain in the world?",
|
||||
"How tall is it in feet?",
|
||||
"When was it first climbed?",
|
||||
"Who was on that expedition?",
|
||||
"What country is the mountain in?",
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(prompts, 1):
|
||||
print(f"Turn {i}:")
|
||||
print(f"User: {prompt}")
|
||||
result = await Runner.run(agent, prompt, session=session)
|
||||
print(f"Assistant: {result.final_output}\n")
|
||||
|
||||
# Show session state after automatic compaction (if triggered)
|
||||
items = await session.get_items()
|
||||
print("=== Session State (Auto Compaction) ===")
|
||||
print(f"Total items: {len(items)}")
|
||||
for item in items:
|
||||
# Some inputs are stored as easy messages (only `role` and `content`).
|
||||
item_type = item.get("type") or ("message" if "role" in item else "unknown")
|
||||
if item_type == "compaction":
|
||||
print(" - compaction (encrypted content)")
|
||||
elif item_type == "message":
|
||||
role = item.get("role", "unknown")
|
||||
print(f" - message ({role})")
|
||||
else:
|
||||
print(f" - {item_type}")
|
||||
print()
|
||||
|
||||
# Manual compaction after inspecting the auto-compacted state.
|
||||
print("=== Manual Compaction ===")
|
||||
await session.run_compaction({"force": True})
|
||||
print("Done")
|
||||
print()
|
||||
|
||||
# Show final session state after manual compaction
|
||||
items = await session.get_items()
|
||||
print("=== Session State (Manual Compaction) ===")
|
||||
print(f"Total items: {len(items)}")
|
||||
for item in items:
|
||||
item_type = item.get("type") or ("message" if "role" in item else "unknown")
|
||||
if item_type == "compaction":
|
||||
print(" - compaction (encrypted content)")
|
||||
elif item_type == "message":
|
||||
role = item.get("role", "unknown")
|
||||
print(f" - message ({role})")
|
||||
else:
|
||||
print(f" - {item_type}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Example demonstrating stateless compaction with store=False.
|
||||
|
||||
In auto mode, OpenAIResponsesCompactionSession uses input-based compaction when
|
||||
responses are not stored on the server.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, ModelSettings, OpenAIResponsesCompactionSession, Runner, SQLiteSession
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an underlying session for storage
|
||||
underlying = SQLiteSession(":memory:")
|
||||
|
||||
# Wrap with compaction session in auto mode. When store=False, this will
|
||||
# compact using the locally stored input items.
|
||||
session = OpenAIResponsesCompactionSession(
|
||||
session_id="demo-session",
|
||||
underlying_session=underlying,
|
||||
model="gpt-4.1",
|
||||
compaction_mode="auto",
|
||||
should_trigger_compaction=lambda ctx: len(ctx["compaction_candidate_items"]) >= 3,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply concisely. Keep answers to 1-2 sentences.",
|
||||
model_settings=ModelSettings(store=False),
|
||||
)
|
||||
|
||||
print("=== Stateless Compaction Session Example ===\n")
|
||||
|
||||
prompts = [
|
||||
"What is the tallest mountain in the world?",
|
||||
"How tall is it in feet?",
|
||||
"When was it first climbed?",
|
||||
"Who was on that expedition?",
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(prompts, 1):
|
||||
print(f"Turn {i}:")
|
||||
print(f"User: {prompt}")
|
||||
result = await Runner.run(agent, prompt, session=session)
|
||||
print(f"Assistant: {result.final_output}\n")
|
||||
|
||||
# Show session state after automatic compaction (if triggered)
|
||||
items = await session.get_items()
|
||||
print("=== Session State (Auto Compaction) ===")
|
||||
print(f"Total items: {len(items)}")
|
||||
for item in items:
|
||||
item_type = item.get("type") or ("message" if "role" in item else "unknown")
|
||||
if item_type == "compaction":
|
||||
print(" - compaction (encrypted content)")
|
||||
elif item_type == "message":
|
||||
role = item.get("role", "unknown")
|
||||
print(f" - message ({role})")
|
||||
else:
|
||||
print(f" - {item_type}")
|
||||
print()
|
||||
|
||||
# Manual compaction in stateless mode.
|
||||
print("=== Manual Compaction ===")
|
||||
await session.run_compaction({"force": True})
|
||||
print("Done")
|
||||
print()
|
||||
|
||||
# Show final session state
|
||||
items = await session.get_items()
|
||||
print("=== Final Session State ===")
|
||||
print(f"Total items: {len(items)}")
|
||||
for item in items:
|
||||
item_type = item.get("type") or ("message" if "role" in item else "unknown")
|
||||
if item_type == "compaction":
|
||||
print(" - compaction (encrypted content)")
|
||||
elif item_type == "message":
|
||||
role = item.get("role", "unknown")
|
||||
print(f" - message ({role})")
|
||||
else:
|
||||
print(f" - {item_type}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,586 @@
|
||||
"""
|
||||
Example demonstrating Dapr State Store session memory functionality.
|
||||
|
||||
This example shows how to use Dapr-backed session memory to maintain conversation
|
||||
history across multiple agent runs with support for various backend stores
|
||||
(Redis, PostgreSQL, MongoDB, etc.).
|
||||
|
||||
WHAT IS DAPR?
|
||||
Dapr (https://dapr.io) is a portable, event-driven runtime that simplifies building
|
||||
resilient applications. Its state management building block provides a unified API
|
||||
for storing data across 30+ databases with built-in telemetry, tracing, encryption, data
|
||||
isolation and lifecycle management via time-to-live (TTL). See: https://docs.dapr.io/developing-applications/building-blocks/state-management/
|
||||
|
||||
WHEN TO USE DaprSession:
|
||||
- Horizontally scaled deployments (multiple agent instances behind a load balancer)
|
||||
- Multi-region requirements (agents run in different geographic regions)
|
||||
- Existing Dapr adoption (your team already uses Dapr for other services)
|
||||
- Backend flexibility (switch state stores without code changes)
|
||||
- Enterprise governance (centralized control over state management policies)
|
||||
|
||||
WHEN TO CONSIDER ALTERNATIVES:
|
||||
- Use SQLiteSession for single-instance agents (desktop app, CLI tool)
|
||||
- Use Session (in-memory) for quick prototypes or short-lived sessions
|
||||
|
||||
PRODUCTION FEATURES (provided by Dapr):
|
||||
- Backend flexibility: 30+ state stores (Redis, PostgreSQL, MongoDB, Cosmos DB, etc.)
|
||||
- Built-in observability: Distributed tracing, metrics, telemetry (zero code)
|
||||
- Data isolation: App-level or namespace-level state scoping for multi-tenancy
|
||||
- TTL support: Automatic session expiration (store-dependent)
|
||||
- Consistency levels: Eventual (faster) or strong (read-after-write guarantee)
|
||||
- State encryption: AES-GCM encryption at the Dapr component level
|
||||
- Cloud-native: Seamless Kubernetes integration (Dapr runs as sidecar)
|
||||
- Cloud Service Provider (CSP) native authentication and authorization support.
|
||||
|
||||
PREREQUISITES:
|
||||
1. Install Dapr CLI: https://docs.dapr.io/getting-started/install-dapr-cli/
|
||||
2. Install Docker (for running Redis and optionally Dapr containers)
|
||||
3. Install openai-agents with dapr in your environment:
|
||||
pip install openai-agents[dapr]
|
||||
4. Use the built-in helper to create components and start containers (Creates ./components with Redis + PostgreSQL and starts containers if Docker is available.):
|
||||
python examples/memory/dapr_session_example.py --setup-env --only-setup
|
||||
5. As always, ensure that the OPENAI_API_KEY environment variable is set.
|
||||
6. Optionally, if planning on using other Dapr features, run: dapr init
|
||||
- This installs Redis, Zipkin, and Placement service locally
|
||||
- Useful for workflows, actors, pub/sub, and other Dapr building blocks that are incredible useful for agents.
|
||||
7. Start dapr sidecar (The app-id is the name of the application that will be running the agent. It can be any name you want. You can check the app-id with `dapr list`.):
|
||||
dapr run --app-id openai-agents-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components
|
||||
|
||||
COMMON ISSUES:
|
||||
- "Health check connection refused (port 3500)": Always use --dapr-http-port 3500
|
||||
when starting Dapr, or set DAPR_HTTP_ENDPOINT="http://localhost:3500"
|
||||
- "State store not found": Ensure component YAML is in --resources-path directory
|
||||
- "Dapr sidecar not reachable": Check with `dapr list` and verify gRPC port 50001
|
||||
|
||||
Important:
|
||||
- If you recreate the PostgreSQL container while daprd stays running, the Postgres state store component
|
||||
may keep an old connection pool and not re-run initialization, leading to errors like
|
||||
"relation \"state\" does not exist". Fix by restarting daprd or triggering a component reload by
|
||||
touching the component YAML under your --resources-path.
|
||||
|
||||
Note: This example clears the session at the start to ensure a clean demonstration.
|
||||
In production, you may want to preserve existing conversation history.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["GRPC_VERBOSITY"] = (
|
||||
"ERROR" # Suppress gRPC warnings caused by the Dapr Python SDK gRPC connection.
|
||||
)
|
||||
|
||||
from agents import Agent, Runner
|
||||
from agents.extensions.memory import (
|
||||
DAPR_CONSISTENCY_EVENTUAL,
|
||||
DAPR_CONSISTENCY_STRONG,
|
||||
DaprSession,
|
||||
)
|
||||
|
||||
grpc_port = os.environ.get("DAPR_GRPC_PORT", "50001")
|
||||
DEFAULT_STATE_STORE = os.environ.get("DAPR_STATE_STORE", "statestore")
|
||||
|
||||
|
||||
async def ping_with_retry(
|
||||
session: DaprSession, timeout_seconds: float = 5.0, interval_seconds: float = 0.5
|
||||
) -> bool:
|
||||
"""Retry session.ping() until success or timeout."""
|
||||
now = asyncio.get_running_loop().time
|
||||
deadline = now() + timeout_seconds
|
||||
while True:
|
||||
if await session.ping():
|
||||
return True
|
||||
print("Dapr sidecar is not available! Retrying...")
|
||||
if now() >= deadline:
|
||||
return False
|
||||
await asyncio.sleep(interval_seconds)
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
print("=== Dapr Session Example ===")
|
||||
print()
|
||||
print("########################################################")
|
||||
print("This example requires Dapr sidecar to be running")
|
||||
print("########################################################")
|
||||
print()
|
||||
print(
|
||||
"Start Dapr with: dapr run --app-id myapp --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components"
|
||||
) # noqa: E501
|
||||
print()
|
||||
|
||||
# Create a Dapr session instance with context manager for automatic cleanup
|
||||
session_id = "dapr_conversation_123"
|
||||
try:
|
||||
# Use async with to automatically close the session on exit
|
||||
async with DaprSession.from_address(
|
||||
session_id,
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
) as session:
|
||||
# Test Dapr connectivity
|
||||
if not await ping_with_retry(session, timeout_seconds=5.0, interval_seconds=0.5):
|
||||
print("Dapr sidecar is not available!")
|
||||
print("Please start Dapr sidecar and try again.")
|
||||
print(
|
||||
"Command: dapr run --app-id myapp --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components"
|
||||
) # noqa: E501
|
||||
return
|
||||
|
||||
print("Connected to Dapr successfully!")
|
||||
print(f"Session ID: {session_id}")
|
||||
print(f"State Store: {DEFAULT_STATE_STORE}")
|
||||
|
||||
# Clear any existing session data for a clean start
|
||||
await session.clear_session()
|
||||
print("Session cleared for clean demonstration.")
|
||||
print("The agent will remember previous messages automatically.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print(
|
||||
"Dapr session automatically handles conversation history with backend flexibility."
|
||||
)
|
||||
|
||||
# Demonstrate session persistence
|
||||
print("\n=== Session Persistence Demo ===")
|
||||
all_items = await session.get_items()
|
||||
print(f"Total messages stored in Dapr: {len(all_items)}")
|
||||
|
||||
# Demonstrate the limit parameter
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
print("Latest 2 items:")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
# Demonstrate session isolation with a new session
|
||||
print("\n=== Session Isolation Demo ===")
|
||||
# Use context manager for the new session too
|
||||
async with DaprSession.from_address(
|
||||
"different_conversation_456",
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
) as new_session:
|
||||
print("Creating a new session with different ID...")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Hello, this is a new conversation!",
|
||||
session=new_session,
|
||||
)
|
||||
print(f"New session response: {result.final_output}")
|
||||
|
||||
# Show that sessions are isolated
|
||||
original_items = await session.get_items()
|
||||
new_items = await new_session.get_items()
|
||||
print(f"Original session has {len(original_items)} items")
|
||||
print(f"New session has {len(new_items)} items")
|
||||
print("Sessions are completely isolated!")
|
||||
|
||||
# Clean up the new session
|
||||
await new_session.clear_session()
|
||||
# No need to call close() - context manager handles it automatically!
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print(
|
||||
"Make sure Dapr sidecar is running with: dapr run --app-id myapp --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components"
|
||||
) # noqa: E501
|
||||
|
||||
|
||||
async def demonstrate_advanced_features():
|
||||
"""Demonstrate advanced Dapr session features."""
|
||||
print("\n=== Advanced Features Demo ===")
|
||||
|
||||
try:
|
||||
# TTL (time-to-live) configuration
|
||||
print("\n1. TTL Configuration:")
|
||||
async with DaprSession.from_address(
|
||||
"ttl_demo_session",
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
ttl=3600, # 1 hour TTL
|
||||
) as ttl_session:
|
||||
if await ttl_session.ping():
|
||||
await Runner.run(
|
||||
Agent(name="Assistant", instructions="Be helpful"),
|
||||
"This message will expire in 1 hour",
|
||||
session=ttl_session,
|
||||
)
|
||||
print("Created session with 1-hour TTL - messages will auto-expire")
|
||||
print("(TTL support depends on the underlying state store)")
|
||||
|
||||
# Consistency levels
|
||||
print("\n2. Consistency Levels:")
|
||||
|
||||
# Eventual consistency (better performance)
|
||||
async with DaprSession.from_address(
|
||||
"eventual_session",
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
consistency=DAPR_CONSISTENCY_EVENTUAL,
|
||||
) as eventual_session:
|
||||
if await eventual_session.ping():
|
||||
print("Eventual consistency: Better performance, may have slight delays")
|
||||
await eventual_session.add_items([{"role": "user", "content": "Test eventual"}])
|
||||
|
||||
# Strong consistency (guaranteed read-after-write)
|
||||
async with DaprSession.from_address(
|
||||
"strong_session",
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
consistency=DAPR_CONSISTENCY_STRONG,
|
||||
) as strong_session:
|
||||
if await strong_session.ping():
|
||||
print("Strong consistency: Guaranteed immediate consistency")
|
||||
await strong_session.add_items([{"role": "user", "content": "Test strong"}])
|
||||
|
||||
# Multi-tenancy example
|
||||
print("\n3. Multi-tenancy with Session Prefixes:")
|
||||
|
||||
def get_tenant_session(tenant_id: str, user_id: str) -> DaprSession:
|
||||
session_id = f"{tenant_id}:{user_id}"
|
||||
return DaprSession.from_address(
|
||||
session_id,
|
||||
state_store_name=DEFAULT_STATE_STORE,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
)
|
||||
|
||||
async with get_tenant_session("tenant-a", "user-123") as tenant_a_session:
|
||||
async with get_tenant_session("tenant-b", "user-123") as tenant_b_session:
|
||||
if await tenant_a_session.ping() and await tenant_b_session.ping():
|
||||
await tenant_a_session.add_items([{"role": "user", "content": "Tenant A data"}])
|
||||
await tenant_b_session.add_items([{"role": "user", "content": "Tenant B data"}])
|
||||
print("Multi-tenant sessions created with isolated data")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Advanced features error: {e}")
|
||||
|
||||
|
||||
async def setup_instructions():
|
||||
"""Print setup instructions for running the example."""
|
||||
print("\n=== Setup Instructions (Multi-store) ===")
|
||||
print("\n1. Create components (Redis + PostgreSQL) in ./components:")
|
||||
print("""
|
||||
# Save as components/statestore-redis.yaml
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore-redis
|
||||
spec:
|
||||
type: state.redis
|
||||
version: v1
|
||||
metadata:
|
||||
- name: redisHost
|
||||
value: localhost:6379
|
||||
- name: redisPassword
|
||||
value: ""
|
||||
|
||||
# Save as components/statestore-postgres.yaml
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore-postgres
|
||||
spec:
|
||||
type: state.postgresql
|
||||
version: v2
|
||||
metadata:
|
||||
- name: connectionString
|
||||
value: "host=localhost user=postgres password=postgres dbname=dapr port=5432"
|
||||
""")
|
||||
print(" You can select which one the main demo uses via env var:")
|
||||
print(" export DAPR_STATE_STORE=statestore-redis # or statestore-postgres")
|
||||
print(" Start both Redis and PostgreSQL for this multi-store demo:")
|
||||
print(" docker run -d -p 6379:6379 redis:7-alpine")
|
||||
print(
|
||||
" docker run -d -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=dapr postgres:16-alpine"
|
||||
)
|
||||
|
||||
print("\n NOTE: Always use secret references for passwords/keys in production!")
|
||||
print(" See: https://docs.dapr.io/operations/components/component-secrets/")
|
||||
|
||||
print("\n2. Start Dapr sidecar:")
|
||||
print(
|
||||
" dapr run --app-id myapp --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components"
|
||||
)
|
||||
print("\n IMPORTANT: Always specify --dapr-http-port 3500 to avoid connection errors!")
|
||||
print(
|
||||
" If you recreate PostgreSQL while daprd is running, restart daprd or touch the component YAML"
|
||||
)
|
||||
print(
|
||||
" to trigger a reload, otherwise you may see 'relation "
|
||||
+ '\\"state\\"'
|
||||
+ " does not exist'."
|
||||
)
|
||||
|
||||
print("\n3. Run this example:")
|
||||
print(" python examples/memory/dapr_session_example.py")
|
||||
|
||||
print("\n Optional: Override store names via env vars:")
|
||||
print(" export DAPR_STATE_STORE=statestore-postgres")
|
||||
print(" export DAPR_STATE_STORE_REDIS=statestore-redis")
|
||||
print(" export DAPR_STATE_STORE_POSTGRES=statestore-postgres")
|
||||
|
||||
print("\n TIP: If you get 'connection refused' errors, set the HTTP endpoint:")
|
||||
print(" export DAPR_HTTP_ENDPOINT='http://localhost:3500'")
|
||||
print(" python examples/memory/dapr_session_example.py")
|
||||
|
||||
print("\n4. For Kubernetes deployment:")
|
||||
print(" Add these annotations to your pod spec:")
|
||||
print(" dapr.io/enabled: 'true'")
|
||||
print(" dapr.io/app-id: 'agents-app'")
|
||||
print(" Then use: dapr_address='localhost:50001' in your code")
|
||||
|
||||
print("\nDocs: Supported state stores and configuration:")
|
||||
print("https://docs.dapr.io/reference/components-reference/supported-state-stores/")
|
||||
|
||||
|
||||
async def demonstrate_multi_store():
|
||||
"""Demonstrate using two different state stores in the same app."""
|
||||
print("\n=== Multi-store Demo (Redis + PostgreSQL) ===")
|
||||
redis_store = os.environ.get("DAPR_STATE_STORE_REDIS", "statestore-redis")
|
||||
pg_store = os.environ.get("DAPR_STATE_STORE_POSTGRES", "statestore-postgres")
|
||||
|
||||
try:
|
||||
async with (
|
||||
DaprSession.from_address(
|
||||
"multi_store_demo:redis",
|
||||
state_store_name=redis_store,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
) as redis_session,
|
||||
DaprSession.from_address(
|
||||
"multi_store_demo:postgres",
|
||||
state_store_name=pg_store,
|
||||
dapr_address=f"localhost:{grpc_port}",
|
||||
) as pg_session,
|
||||
):
|
||||
ok_redis = await ping_with_retry(
|
||||
redis_session, timeout_seconds=5.0, interval_seconds=0.5
|
||||
)
|
||||
ok_pg = await ping_with_retry(pg_session, timeout_seconds=5.0, interval_seconds=0.5)
|
||||
if not (ok_redis and ok_pg):
|
||||
print(
|
||||
"----------------------------------------\n"
|
||||
"ERROR: One or both state stores are unavailable. Ensure both components exist and are running. \n"
|
||||
"Run with --setup-env to create the components and start the containers.\n"
|
||||
"----------------------------------------\n"
|
||||
)
|
||||
print(f"Redis store name: {redis_store}")
|
||||
print(f"PostgreSQL store name: {pg_store}")
|
||||
return
|
||||
|
||||
await redis_session.clear_session()
|
||||
await pg_session.clear_session()
|
||||
|
||||
await redis_session.add_items([{"role": "user", "content": "Hello from Redis"}])
|
||||
await pg_session.add_items([{"role": "user", "content": "Hello from PostgreSQL"}])
|
||||
|
||||
r_items = await redis_session.get_items()
|
||||
p_items = await pg_session.get_items()
|
||||
|
||||
r_example = r_items[-1]["content"] if r_items else "empty" # type: ignore[typeddict-item]
|
||||
p_example = p_items[-1]["content"] if p_items else "empty" # type: ignore[typeddict-item]
|
||||
|
||||
print(f"{redis_store}: {len(r_items)} items; example: {r_example}")
|
||||
print(f"{pg_store}: {len(p_items)} items; example: {p_example}")
|
||||
print("Data is isolated per state store.")
|
||||
except Exception as e:
|
||||
print(f"Multi-store demo error: {e}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# --- Setup Helper Functions --
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _write_text_file(path: Path, content: str, overwrite: bool) -> None:
|
||||
if path.exists() and not overwrite:
|
||||
return
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
return shutil.which("docker") is not None
|
||||
|
||||
|
||||
def _container_running(name: str):
|
||||
if not _docker_available():
|
||||
return None
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "-f", "{{.State.Running}}", name],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return result.stdout.strip().lower() == "true"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_container(name: str, run_args: list[str]) -> None:
|
||||
if not _docker_available():
|
||||
raise SystemExit(
|
||||
"Docker is required to automatically start containers for '"
|
||||
+ name
|
||||
+ "'.\nInstall Docker: https://docs.docker.com/get-docker/\n"
|
||||
+ "Alternatively, start the container manually and re-run with --setup-env."
|
||||
)
|
||||
status = _container_running(name)
|
||||
if status is True:
|
||||
print(f"Container '{name}' already running.")
|
||||
return
|
||||
if status is False:
|
||||
subprocess.run(["docker", "start", name], check=False)
|
||||
print(f"Started existing container '{name}'.")
|
||||
return
|
||||
subprocess.run(["docker", "run", "-d", "--name", name, *run_args], check=False)
|
||||
print(f"Created and started container '{name}'.")
|
||||
|
||||
|
||||
def setup_environment(components_dir: str = "./components", overwrite: bool = False) -> None:
|
||||
"""Create Redis/PostgreSQL component files and start containers if available."""
|
||||
components_path = Path(components_dir)
|
||||
components_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
redis_component = """
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore-redis
|
||||
spec:
|
||||
type: state.redis
|
||||
version: v1
|
||||
metadata:
|
||||
- name: redisHost
|
||||
value: localhost:6379
|
||||
- name: redisPassword
|
||||
value: ""
|
||||
""".lstrip()
|
||||
|
||||
postgres_component = """
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore-postgres
|
||||
spec:
|
||||
type: state.postgresql
|
||||
version: v2
|
||||
metadata:
|
||||
- name: connectionString
|
||||
value: "host=localhost user=postgres password=postgres dbname=dapr port=5432"
|
||||
""".lstrip()
|
||||
|
||||
default_component = """
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore
|
||||
spec:
|
||||
type: state.redis
|
||||
version: v1
|
||||
metadata:
|
||||
- name: redisHost
|
||||
value: localhost:6379
|
||||
- name: redisPassword
|
||||
value: ""
|
||||
""".lstrip()
|
||||
|
||||
_write_text_file(components_path / "statestore-redis.yaml", redis_component, overwrite)
|
||||
_write_text_file(components_path / "statestore-postgres.yaml", postgres_component, overwrite)
|
||||
_write_text_file(components_path / "statestore.yaml", default_component, overwrite)
|
||||
|
||||
print(f"Components written under: {components_path.resolve()}")
|
||||
|
||||
_ensure_container("dapr_redis", ["-p", "6379:6379", "redis:7-alpine"])
|
||||
_ensure_container(
|
||||
"dapr_postgres",
|
||||
[
|
||||
"-p",
|
||||
"5432:5432",
|
||||
"-e",
|
||||
"POSTGRES_USER=postgres",
|
||||
"-e",
|
||||
"POSTGRES_PASSWORD=postgres",
|
||||
"-e",
|
||||
"POSTGRES_DB=dapr",
|
||||
"postgres:16-alpine",
|
||||
],
|
||||
)
|
||||
print("Environment setup complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Dapr session example")
|
||||
parser.add_argument(
|
||||
"--setup-env",
|
||||
action="store_true",
|
||||
help="Create ./components and add Redis/PostgreSQL components; start containers if possible.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--components-dir",
|
||||
default="./components",
|
||||
help="Path to Dapr components directory (default: ./components)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Overwrite existing component files if present.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only-setup",
|
||||
action="store_true",
|
||||
help="Exit after setting up the environment.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.setup_env:
|
||||
setup_environment(args.components_dir, overwrite=args.overwrite)
|
||||
if args.only_setup:
|
||||
raise SystemExit(0)
|
||||
|
||||
asyncio.run(setup_instructions())
|
||||
asyncio.run(main())
|
||||
asyncio.run(demonstrate_advanced_features())
|
||||
asyncio.run(demonstrate_multi_store())
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Example demonstrating encrypted session memory functionality.
|
||||
|
||||
This example shows how to use encrypted session memory to maintain conversation history
|
||||
across multiple agent runs with automatic encryption and TTL-based expiration.
|
||||
The EncryptedSession wrapper provides transparent encryption over any underlying session.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
from agents.extensions.memory import EncryptedSession
|
||||
from agents.extensions.memory.encrypt_session import EncryptedEnvelope
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create an underlying session (SQLiteSession in this example)
|
||||
session_id = "conversation_123"
|
||||
underlying_session = SQLiteSession(session_id)
|
||||
|
||||
# Wrap with encrypted session for automatic encryption and TTL
|
||||
session = EncryptedSession(
|
||||
session_id=session_id,
|
||||
underlying_session=underlying_session,
|
||||
encryption_key="my-secret-encryption-key",
|
||||
ttl=3600, # 1 hour TTL for messages
|
||||
)
|
||||
|
||||
print("=== Encrypted Session Example ===")
|
||||
print("The agent will remember previous messages automatically with encryption.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print("All conversation history was automatically encrypted and stored securely.")
|
||||
|
||||
# Demonstrate the limit parameter - get only the latest 2 items
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
print("Latest 2 items (automatically decrypted):")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nFetched {len(latest_items)} out of total conversation history.")
|
||||
|
||||
# Get all items to show the difference
|
||||
all_items = await session.get_items()
|
||||
print(f"Total items in session: {len(all_items)}")
|
||||
|
||||
# Show that underlying storage is encrypted
|
||||
print("\n=== Encryption Demo ===")
|
||||
print("Checking underlying storage to verify encryption...")
|
||||
raw_items = await underlying_session.get_items()
|
||||
print("Raw encrypted items in underlying storage:")
|
||||
for i, item in enumerate(raw_items, 1):
|
||||
if isinstance(item, dict) and item.get("__enc__") == 1:
|
||||
enc_item = cast(EncryptedEnvelope, item)
|
||||
print(
|
||||
f" {i}. Encrypted envelope: __enc__={enc_item['__enc__']}, "
|
||||
f"payload length={len(enc_item['payload'])}"
|
||||
)
|
||||
else:
|
||||
print(f" {i}. Unencrypted item: {item}")
|
||||
|
||||
print(f"\nAll {len(raw_items)} items are stored encrypted with TTL-based expiration.")
|
||||
|
||||
# Clean up
|
||||
underlying_session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
File-backed session example with human-in-the-loop tool approval.
|
||||
|
||||
This mirrors the JS `file-hitl.ts` sample: a session persisted on disk and tools that
|
||||
require approval before execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from agents import Agent, Runner, function_tool
|
||||
from agents.run_context import RunContextWrapper
|
||||
from agents.run_state import RunState
|
||||
from examples.auto_mode import confirm_with_fallback, input_with_fallback, is_auto_mode
|
||||
|
||||
from .file_session import FileSession
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
user_context = {"user_id": "101"}
|
||||
|
||||
customer_directory: dict[str, str] = {
|
||||
"101": (
|
||||
"Customer Kaz S. (tier gold) can be reached at +1-415-555-AAAA. "
|
||||
"Notes: Prefers SMS follow ups and values concise summaries."
|
||||
),
|
||||
"104": (
|
||||
"Customer Yu S. (tier platinum) can be reached at +1-415-555-BBBB. "
|
||||
"Notes: Recently reported sync issues. Flagged for a proactive onboarding call."
|
||||
),
|
||||
"205": (
|
||||
"Customer Ken S. (tier standard) can be reached at +1-415-555-CCCC. "
|
||||
"Notes: Interested in automation tutorials sent last week."
|
||||
),
|
||||
}
|
||||
|
||||
lookup_customer_profile = create_lookup_customer_profile_tool(directory=customer_directory)
|
||||
|
||||
instructions = (
|
||||
"You assist support agents. For every user turn you must call lookup_customer_profile. "
|
||||
"If a tool reports a transient failure, request approval and retry the same call once before "
|
||||
"responding. Keep responses under three sentences."
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="File HITL assistant",
|
||||
instructions=instructions,
|
||||
tools=[lookup_customer_profile],
|
||||
)
|
||||
|
||||
session = FileSession(dir="examples/memory/tmp")
|
||||
session_id = await session.get_session_id()
|
||||
print(f"Session id: {session_id}")
|
||||
print("Enter a message to chat with the agent. Submit an empty line to exit.")
|
||||
auto_mode = is_auto_mode()
|
||||
|
||||
saved_state = await session.load_state_json()
|
||||
if saved_state:
|
||||
print("Found saved run state. Resuming pending interruptions before new input.")
|
||||
try:
|
||||
state = await RunState.from_json(agent, saved_state, context_override=user_context)
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
while result.interruptions:
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
args = format_tool_arguments(interruption)
|
||||
approved = await prompt_yes_no(
|
||||
f"Agent {interruption.agent.name} wants to call {interruption.name} with {args or 'no arguments'}"
|
||||
)
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
print("Approved tool call.")
|
||||
else:
|
||||
state.reject(interruption)
|
||||
print("Rejected tool call.")
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
await session.save_state_json(result.to_state().to_json())
|
||||
reply = result.final_output or "[No final output produced]"
|
||||
print(f"Assistant (resumed): {reply}\n")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"Failed to resume saved state: {exc}. Starting a new session.")
|
||||
|
||||
while True:
|
||||
if auto_mode:
|
||||
user_message = input_with_fallback("You: ", "Summarize the customer profile.")
|
||||
else:
|
||||
print("You: ", end="", flush=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
user_message = await loop.run_in_executor(None, input)
|
||||
if not user_message.strip():
|
||||
break
|
||||
|
||||
result = await Runner.run(agent, user_message, session=session, context=user_context)
|
||||
while result.interruptions:
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
args = format_tool_arguments(interruption)
|
||||
approved = await prompt_yes_no(
|
||||
f"Agent {interruption.agent.name} wants to call {interruption.name} with {args or 'no arguments'}"
|
||||
)
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
print("Approved tool call.")
|
||||
else:
|
||||
state.reject(interruption)
|
||||
print("Rejected tool call.")
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
await session.save_state_json(result.to_state().to_json())
|
||||
|
||||
reply = result.final_output or "[No final output produced]"
|
||||
print(f"Assistant: {reply}\n")
|
||||
if auto_mode:
|
||||
break
|
||||
|
||||
|
||||
def create_lookup_customer_profile_tool(
|
||||
*,
|
||||
directory: dict[str, str],
|
||||
missing_customer_message: str = "No customer found for that id.",
|
||||
):
|
||||
@function_tool(
|
||||
name_override="lookup_customer_profile",
|
||||
description_override="Look up stored profile details for a customer by their internal id.",
|
||||
needs_approval=True,
|
||||
)
|
||||
def lookup_customer_profile(ctx: RunContextWrapper[Any]) -> str:
|
||||
return directory.get(ctx.context.get("user_id"), missing_customer_message)
|
||||
|
||||
return lookup_customer_profile
|
||||
|
||||
|
||||
def format_tool_arguments(interruption: Any) -> str:
|
||||
args = getattr(interruption, "arguments", None)
|
||||
if args is None:
|
||||
return ""
|
||||
if isinstance(args, str):
|
||||
return args
|
||||
try:
|
||||
return json.dumps(args)
|
||||
except Exception:
|
||||
return str(args)
|
||||
|
||||
|
||||
async def prompt_yes_no(question: str) -> bool:
|
||||
return confirm_with_fallback(f"{question} (y/n): ", default=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Simple file-backed session implementation for examples.
|
||||
|
||||
Persists conversation history as JSON on disk so runs can resume across processes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from agents.memory.session import Session
|
||||
from agents.memory.session_settings import SessionSettings
|
||||
|
||||
|
||||
class FileSession(Session):
|
||||
"""Persist session items to a JSON file on disk."""
|
||||
|
||||
session_settings: SessionSettings | None = None
|
||||
|
||||
def __init__(self, *, dir: str | Path | None = None, session_id: str | None = None) -> None:
|
||||
self._dir = Path(dir) if dir is not None else Path.cwd() / ".agents-sessions"
|
||||
self.session_id = session_id or ""
|
||||
# Ensure the directory exists up front so subsequent file operations do not race.
|
||||
self._dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async def _ensure_session_id(self) -> str:
|
||||
if not self.session_id:
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
# Prefix with wall-clock time so recent sessions are easy to spot on disk.
|
||||
self.session_id = f"{timestamp}-{uuid4().hex[:12]}"
|
||||
await asyncio.to_thread(self._dir.mkdir, parents=True, exist_ok=True)
|
||||
file_path = self._items_path(self.session_id)
|
||||
if not file_path.exists():
|
||||
await asyncio.to_thread(file_path.write_text, "[]", encoding="utf-8")
|
||||
return self.session_id
|
||||
|
||||
async def get_session_id(self) -> str:
|
||||
"""Return the session id, creating one if needed."""
|
||||
return await self._ensure_session_id()
|
||||
|
||||
async def get_items(self, limit: int | None = None) -> list[Any]:
|
||||
session_id = await self._ensure_session_id()
|
||||
items = await self._read_items(session_id)
|
||||
if limit is not None and limit >= 0:
|
||||
return items[-limit:]
|
||||
return items
|
||||
|
||||
async def add_items(self, items: list[Any]) -> None:
|
||||
if not items:
|
||||
return
|
||||
session_id = await self._ensure_session_id()
|
||||
current = await self._read_items(session_id)
|
||||
# Deep-copy via JSON to avoid persisting live references that might mutate later.
|
||||
cloned = json.loads(json.dumps(items))
|
||||
await self._write_items(session_id, current + cloned)
|
||||
|
||||
async def pop_item(self) -> Any | None:
|
||||
session_id = await self._ensure_session_id()
|
||||
items = await self._read_items(session_id)
|
||||
if not items:
|
||||
return None
|
||||
popped = items.pop()
|
||||
await self._write_items(session_id, items)
|
||||
return popped
|
||||
|
||||
async def clear_session(self) -> None:
|
||||
if not self.session_id:
|
||||
return
|
||||
file_path = self._items_path(self.session_id)
|
||||
state_path = self._state_path(self.session_id)
|
||||
try:
|
||||
await asyncio.to_thread(file_path.unlink)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
try:
|
||||
await asyncio.to_thread(state_path.unlink)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.session_id = ""
|
||||
|
||||
def _items_path(self, session_id: str) -> Path:
|
||||
return self._dir / f"{session_id}.json"
|
||||
|
||||
def _state_path(self, session_id: str) -> Path:
|
||||
return self._dir / f"{session_id}-state.json"
|
||||
|
||||
async def _read_items(self, session_id: str) -> list[Any]:
|
||||
file_path = self._items_path(session_id)
|
||||
try:
|
||||
data = await asyncio.to_thread(file_path.read_text, "utf-8")
|
||||
parsed = json.loads(data)
|
||||
return parsed if isinstance(parsed, list) else []
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
|
||||
async def _write_items(self, session_id: str, items: list[Any]) -> None:
|
||||
file_path = self._items_path(session_id)
|
||||
payload = json.dumps(items, indent=2, ensure_ascii=False)
|
||||
await asyncio.to_thread(self._dir.mkdir, parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(file_path.write_text, payload, encoding="utf-8")
|
||||
|
||||
async def load_state_json(self) -> dict[str, Any] | None:
|
||||
"""Load a previously saved RunState JSON payload, if present."""
|
||||
session_id = await self._ensure_session_id()
|
||||
state_path = self._state_path(session_id)
|
||||
try:
|
||||
data = await asyncio.to_thread(state_path.read_text, "utf-8")
|
||||
parsed = json.loads(data)
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
async def save_state_json(self, state: dict[str, Any]) -> None:
|
||||
"""Persist the serialized RunState JSON payload alongside session items."""
|
||||
session_id = await self._ensure_session_id()
|
||||
state_path = self._state_path(session_id)
|
||||
payload = json.dumps(state, indent=2, ensure_ascii=False)
|
||||
await asyncio.to_thread(self._dir.mkdir, parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(state_path.write_text, payload, encoding="utf-8")
|
||||
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
Scenario that exercises HITL approvals, rehydration, and rejections across sessions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openai.types.shared import Reasoning
|
||||
|
||||
from agents import Agent, Model, ModelSettings, OpenAIConversationsSession, Runner, function_tool
|
||||
from agents.items import TResponseInputItem
|
||||
|
||||
from .file_session import FileSession
|
||||
|
||||
TOOL_ECHO = "approved_echo"
|
||||
TOOL_NOTE = "approved_note"
|
||||
REJECTION_OUTPUT = "Tool execution was not approved."
|
||||
USER_MESSAGES = [
|
||||
"Fetch profile for customer 104.",
|
||||
"Update note for customer 104.",
|
||||
"Delete note for customer 104.",
|
||||
]
|
||||
|
||||
|
||||
def tool_output_for(name: str, message: str) -> str:
|
||||
if name == TOOL_ECHO:
|
||||
return f"approved:{message}"
|
||||
if name == TOOL_NOTE:
|
||||
return f"approved_note:{message}"
|
||||
raise ValueError(f"Unknown tool name: {name}")
|
||||
|
||||
|
||||
@function_tool(
|
||||
name_override=TOOL_ECHO,
|
||||
description_override="Echoes back the provided query after approval.",
|
||||
needs_approval=True,
|
||||
)
|
||||
def approval_echo(query: str) -> str:
|
||||
"""Return the approved echo payload."""
|
||||
return tool_output_for(TOOL_ECHO, query)
|
||||
|
||||
|
||||
@function_tool(
|
||||
name_override=TOOL_NOTE,
|
||||
description_override="Records the provided query after approval.",
|
||||
needs_approval=True,
|
||||
)
|
||||
def approval_note(query: str) -> str:
|
||||
"""Return the approved note payload."""
|
||||
return tool_output_for(TOOL_NOTE, query)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScenarioStep:
|
||||
name: str
|
||||
message: str
|
||||
tool_name: str
|
||||
approval: str
|
||||
expected_output: str
|
||||
|
||||
|
||||
async def run_scenario_step(
|
||||
session: Any,
|
||||
label: str,
|
||||
step: ScenarioStep,
|
||||
*,
|
||||
model: str | Model | None = None,
|
||||
) -> None:
|
||||
agent = Agent(
|
||||
name=f"{label} HITL scenario",
|
||||
instructions=(
|
||||
f"You must call {step.tool_name} exactly once before responding. "
|
||||
"Pass the user input as the 'query' argument."
|
||||
),
|
||||
tools=[approval_echo, approval_note],
|
||||
model=model,
|
||||
model_settings=ModelSettings(
|
||||
tool_choice=step.tool_name, reasoning=Reasoning(effort="none")
|
||||
),
|
||||
tool_use_behavior="stop_on_first_tool",
|
||||
)
|
||||
|
||||
result = await Runner.run(agent, step.message, session=session)
|
||||
if not result.interruptions:
|
||||
raise RuntimeError(f"[{label}] expected at least one tool approval.")
|
||||
|
||||
while result.interruptions:
|
||||
state = result.to_state()
|
||||
for interruption in result.interruptions:
|
||||
if step.approval == "reject":
|
||||
state.reject(interruption)
|
||||
else:
|
||||
state.approve(interruption)
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
|
||||
if result.final_output is None:
|
||||
raise RuntimeError(f"[{label}] expected a final output after approval.")
|
||||
if step.approval != "reject" and result.final_output != step.expected_output:
|
||||
raise RuntimeError(
|
||||
f"[{label}] expected final output '{step.expected_output}' but got "
|
||||
f"'{result.final_output}'."
|
||||
)
|
||||
|
||||
items = await session.get_items()
|
||||
tool_results = [item for item in items if get_item_type(item) == "function_call_output"]
|
||||
user_messages = [item for item in items if get_user_text(item) == step.message]
|
||||
last_tool_call = find_last_item(items, is_function_call)
|
||||
last_tool_result = find_last_item(items, is_function_call_output)
|
||||
|
||||
if not tool_results:
|
||||
raise RuntimeError(f"[{label}] expected tool outputs in session history.")
|
||||
if not user_messages:
|
||||
raise RuntimeError(f"[{label}] expected user input in session history.")
|
||||
if not last_tool_call:
|
||||
raise RuntimeError(f"[{label}] expected a tool call in session history.")
|
||||
if last_tool_call.get("name") != step.tool_name:
|
||||
raise RuntimeError(
|
||||
f"[{label}] expected tool call '{step.tool_name}' but got '{last_tool_call.get('name')}'."
|
||||
)
|
||||
if not last_tool_result:
|
||||
raise RuntimeError(f"[{label}] expected a tool result in session history.")
|
||||
|
||||
tool_call_id = extract_call_id(last_tool_call)
|
||||
tool_result_call_id = extract_call_id(last_tool_result)
|
||||
if tool_call_id and tool_result_call_id and tool_result_call_id != tool_call_id:
|
||||
raise RuntimeError(
|
||||
f"[{label}] expected tool result call_id '{tool_call_id}' but got '{tool_result_call_id}'."
|
||||
)
|
||||
|
||||
tool_output_text = format_output(last_tool_result.get("output"))
|
||||
if tool_output_text != step.expected_output:
|
||||
raise RuntimeError(
|
||||
f"[{label}] expected tool output '{step.expected_output}' but got '{tool_output_text}'."
|
||||
)
|
||||
|
||||
log_session_summary(items, label)
|
||||
print(f"[{label}] final output: {result.final_output} (items: {len(items)})")
|
||||
|
||||
|
||||
async def run_file_session_scenario(*, model: str | Model | None = None) -> None:
|
||||
tmp_root = Path.cwd() / "tmp"
|
||||
tmp_root.mkdir(parents=True, exist_ok=True)
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="hitl-scenario-", dir=tmp_root))
|
||||
session = FileSession(dir=temp_dir)
|
||||
session_id = await session.get_session_id()
|
||||
session_file = temp_dir / f"{session_id}.json"
|
||||
rehydrated_session: FileSession | None = None
|
||||
|
||||
print(f"[FileSession] session id: {session_id}")
|
||||
print(f"[FileSession] file: {session_file}")
|
||||
print("[FileSession] cleanup: always")
|
||||
|
||||
steps = [
|
||||
ScenarioStep(
|
||||
name="turn 1",
|
||||
message=USER_MESSAGES[0],
|
||||
tool_name=TOOL_ECHO,
|
||||
approval="approve",
|
||||
expected_output=tool_output_for(TOOL_ECHO, USER_MESSAGES[0]),
|
||||
),
|
||||
ScenarioStep(
|
||||
name="turn 2 (rehydrated)",
|
||||
message=USER_MESSAGES[1],
|
||||
tool_name=TOOL_NOTE,
|
||||
approval="approve",
|
||||
expected_output=tool_output_for(TOOL_NOTE, USER_MESSAGES[1]),
|
||||
),
|
||||
ScenarioStep(
|
||||
name="turn 3 (rejected)",
|
||||
message=USER_MESSAGES[2],
|
||||
tool_name=TOOL_ECHO,
|
||||
approval="reject",
|
||||
expected_output=REJECTION_OUTPUT,
|
||||
),
|
||||
]
|
||||
|
||||
try:
|
||||
await run_scenario_step(
|
||||
session,
|
||||
f"FileSession {steps[0].name}",
|
||||
steps[0],
|
||||
model=model,
|
||||
)
|
||||
rehydrated_session = FileSession(dir=temp_dir, session_id=session_id)
|
||||
print(f"[FileSession] rehydrated session id: {session_id}")
|
||||
await run_scenario_step(
|
||||
rehydrated_session,
|
||||
f"FileSession {steps[1].name}",
|
||||
steps[1],
|
||||
model=model,
|
||||
)
|
||||
await run_scenario_step(
|
||||
rehydrated_session,
|
||||
f"FileSession {steps[2].name}",
|
||||
steps[2],
|
||||
model=model,
|
||||
)
|
||||
finally:
|
||||
await (rehydrated_session or session).clear_session()
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
async def run_openai_session_scenario(*, model: str | Model | None = None) -> None:
|
||||
existing_session_id = os.environ.get("OPENAI_SESSION_ID")
|
||||
session = OpenAIConversationsSession(conversation_id=existing_session_id)
|
||||
session_id = await get_conversation_id(session)
|
||||
should_keep = bool(os.environ.get("KEEP_OPENAI_SESSION") or existing_session_id)
|
||||
|
||||
if existing_session_id:
|
||||
print(f"[OpenAIConversationsSession] reuse session id: {session_id}")
|
||||
else:
|
||||
print(f"[OpenAIConversationsSession] new session id: {session_id}")
|
||||
print(f"[OpenAIConversationsSession] cleanup: {'skip' if should_keep else 'delete'}")
|
||||
|
||||
steps = [
|
||||
ScenarioStep(
|
||||
name="turn 1",
|
||||
message=USER_MESSAGES[0],
|
||||
tool_name=TOOL_ECHO,
|
||||
approval="approve",
|
||||
expected_output=tool_output_for(TOOL_ECHO, USER_MESSAGES[0]),
|
||||
),
|
||||
ScenarioStep(
|
||||
name="turn 2 (rehydrated)",
|
||||
message=USER_MESSAGES[1],
|
||||
tool_name=TOOL_NOTE,
|
||||
approval="approve",
|
||||
expected_output=tool_output_for(TOOL_NOTE, USER_MESSAGES[1]),
|
||||
),
|
||||
ScenarioStep(
|
||||
name="turn 3 (rejected)",
|
||||
message=USER_MESSAGES[2],
|
||||
tool_name=TOOL_ECHO,
|
||||
approval="reject",
|
||||
expected_output=REJECTION_OUTPUT,
|
||||
),
|
||||
]
|
||||
|
||||
await run_scenario_step(
|
||||
session,
|
||||
f"OpenAIConversationsSession {steps[0].name}",
|
||||
steps[0],
|
||||
model=model,
|
||||
)
|
||||
|
||||
rehydrated_session = OpenAIConversationsSession(conversation_id=session_id)
|
||||
print(f"[OpenAIConversationsSession] rehydrated session id: {session_id}")
|
||||
await run_scenario_step(
|
||||
rehydrated_session,
|
||||
f"OpenAIConversationsSession {steps[1].name}",
|
||||
steps[1],
|
||||
model=model,
|
||||
)
|
||||
await run_scenario_step(
|
||||
rehydrated_session,
|
||||
f"OpenAIConversationsSession {steps[2].name}",
|
||||
steps[2],
|
||||
model=model,
|
||||
)
|
||||
|
||||
if should_keep:
|
||||
print(f"[OpenAIConversationsSession] kept session id: {session_id}")
|
||||
return
|
||||
|
||||
print(f"[OpenAIConversationsSession] deleting session id: {session_id}")
|
||||
await rehydrated_session.clear_session()
|
||||
|
||||
|
||||
async def get_conversation_id(session: OpenAIConversationsSession) -> str:
|
||||
return await session._get_session_id()
|
||||
|
||||
|
||||
def get_user_text(item: TResponseInputItem) -> str | None:
|
||||
if not isinstance(item, dict) or item.get("role") != "user":
|
||||
return None
|
||||
|
||||
content = item.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if not isinstance(content, list):
|
||||
return None
|
||||
|
||||
parts = []
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get("type") == "input_text":
|
||||
parts.append(part.get("text", ""))
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def get_item_type(item: TResponseInputItem) -> str:
|
||||
if isinstance(item, dict):
|
||||
return item.get("type") or ("message" if "role" in item else "unknown")
|
||||
return "unknown"
|
||||
|
||||
|
||||
def is_function_call(item: TResponseInputItem) -> bool:
|
||||
return isinstance(item, dict) and item.get("type") == "function_call"
|
||||
|
||||
|
||||
def is_function_call_output(item: TResponseInputItem) -> bool:
|
||||
return isinstance(item, dict) and item.get("type") == "function_call_output"
|
||||
|
||||
|
||||
def find_last_item(items: list[TResponseInputItem], predicate: Any) -> dict[str, Any] | None:
|
||||
for index in range(len(items) - 1, -1, -1):
|
||||
item = items[index]
|
||||
if predicate(item):
|
||||
return item # type: ignore[return-value]
|
||||
return None
|
||||
|
||||
|
||||
def extract_call_id(item: dict[str, Any]) -> str | None:
|
||||
return cast_str(item.get("call_id") or item.get("id"))
|
||||
|
||||
|
||||
def cast_str(value: Any) -> str | None:
|
||||
return value if isinstance(value, str) else None
|
||||
|
||||
|
||||
def log_session_summary(items: list[TResponseInputItem], label: str) -> None:
|
||||
type_counts: dict[str, int] = {}
|
||||
for item in items:
|
||||
item_type = get_item_type(item)
|
||||
type_counts[item_type] = type_counts.get(item_type, 0) + 1
|
||||
|
||||
type_summary = " ".join(f"{item_type}={count}" for item_type, count in type_counts.items())
|
||||
|
||||
summary_suffix = f" ({type_summary})" if type_summary else ""
|
||||
print(f"[{label}] session summary: items={len(items)}{summary_suffix}")
|
||||
|
||||
user_text = None
|
||||
for index in range(len(items) - 1, -1, -1):
|
||||
user_text = get_user_text(items[index])
|
||||
if user_text:
|
||||
break
|
||||
if user_text:
|
||||
print(f"[{label}] user: {truncate_text(user_text)}")
|
||||
|
||||
tool_call = find_last_item(items, is_function_call)
|
||||
if tool_call:
|
||||
args = truncate_text(str(tool_call.get("arguments", "")))
|
||||
call_id = extract_call_id(tool_call)
|
||||
call_id_label = f" call_id={call_id}" if call_id else ""
|
||||
args_label = f" args={args}" if args else ""
|
||||
print(f"[{label}] tool call: {tool_call.get('name')}{call_id_label}{args_label}")
|
||||
|
||||
tool_result = find_last_item(items, is_function_call_output)
|
||||
if tool_result:
|
||||
output = truncate_text(format_output(tool_result.get("output")))
|
||||
call_id = extract_call_id(tool_result)
|
||||
call_id_label = f" call_id={call_id}" if call_id else ""
|
||||
output_label = f" output={output}" if output else ""
|
||||
print(f"[{label}] tool result:{call_id_label}{output_label}")
|
||||
|
||||
|
||||
def format_output(output: Any) -> str:
|
||||
if isinstance(output, str):
|
||||
return output
|
||||
if output is None:
|
||||
return ""
|
||||
if isinstance(output, list):
|
||||
text_parts = []
|
||||
for entry in output:
|
||||
if isinstance(entry, dict) and entry.get("type") == "input_text":
|
||||
text_parts.append(entry.get("text", ""))
|
||||
if text_parts:
|
||||
return "".join(text_parts)
|
||||
try:
|
||||
return json.dumps(output)
|
||||
except TypeError:
|
||||
return str(output)
|
||||
|
||||
|
||||
def truncate_text(text: str, max_length: int = 140) -> str:
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
suffix = "..."
|
||||
if max_length <= len(suffix):
|
||||
return suffix
|
||||
return f"{text[: max_length - len(suffix)]}{suffix}"
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
if not os.environ.get("OPENAI_API_KEY"):
|
||||
print("OPENAI_API_KEY must be set to run the HITL session scenario.")
|
||||
raise SystemExit(1)
|
||||
|
||||
model_override = os.environ.get("HITL_MODEL", "gpt-5.6-sol")
|
||||
if model_override:
|
||||
print(f"Model: {model_override}")
|
||||
|
||||
await run_file_session_scenario(model=model_override)
|
||||
await run_openai_session_scenario(model=model_override)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Example demonstrating SQLite in-memory session with human-in-the-loop (HITL) tool approval.
|
||||
|
||||
This example shows how to use SQLite in-memory session memory combined with
|
||||
human-in-the-loop tool approval. The session maintains conversation history while
|
||||
requiring approval for specific tool calls.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, SQLiteSession, function_tool
|
||||
from examples.auto_mode import confirm_with_fallback, input_with_fallback, is_auto_mode
|
||||
|
||||
|
||||
async def _needs_approval(_ctx, _params, _call_id) -> bool:
|
||||
"""Always require approval for weather tool."""
|
||||
return True
|
||||
|
||||
|
||||
@function_tool(needs_approval=_needs_approval)
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location.
|
||||
|
||||
Args:
|
||||
location: The location to get weather for
|
||||
|
||||
Returns:
|
||||
Weather information as a string
|
||||
"""
|
||||
# Simulated weather data
|
||||
weather_data = {
|
||||
"san francisco": "Foggy, 58°F",
|
||||
"oakland": "Sunny, 72°F",
|
||||
"new york": "Rainy, 65°F",
|
||||
}
|
||||
# Check if any city name is in the provided location string
|
||||
location_lower = location.lower()
|
||||
for city, weather in weather_data.items():
|
||||
if city in location_lower:
|
||||
return weather
|
||||
return f"Weather data not available for {location}"
|
||||
|
||||
|
||||
async def prompt_yes_no(question: str) -> bool:
|
||||
"""Prompt user for yes/no answer.
|
||||
|
||||
Args:
|
||||
question: The question to ask
|
||||
|
||||
Returns:
|
||||
True if user answered yes, False otherwise
|
||||
"""
|
||||
return confirm_with_fallback(f"\n{question} (y/n): ", default=True)
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent with a tool that requires approval
|
||||
agent = Agent(
|
||||
name="HITL Assistant",
|
||||
instructions="You help users with information. Always use available tools when appropriate. Keep responses concise.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Create an in-memory SQLite session instance that will persist across runs
|
||||
session = SQLiteSession(":memory:")
|
||||
session_id = session.session_id
|
||||
|
||||
print("=== Memory Session + HITL Example ===")
|
||||
print(f"Session id: {session_id}")
|
||||
print("Enter a message to chat with the agent. Submit an empty line to exit.")
|
||||
print("The agent will ask for approval before using tools.\n")
|
||||
|
||||
auto_mode = is_auto_mode()
|
||||
|
||||
while True:
|
||||
# Get user input
|
||||
if auto_mode:
|
||||
user_message = input_with_fallback("You: ", "What's the weather in Oakland?")
|
||||
else:
|
||||
print("You: ", end="", flush=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
user_message = await loop.run_in_executor(None, input)
|
||||
|
||||
if not user_message.strip():
|
||||
break
|
||||
|
||||
# Run the agent
|
||||
result = await Runner.run(agent, user_message, session=session)
|
||||
|
||||
# Handle interruptions (tool approvals)
|
||||
while result.interruptions:
|
||||
# Get the run state
|
||||
state = result.to_state()
|
||||
|
||||
for interruption in result.interruptions:
|
||||
tool_name = interruption.name or "Unknown tool"
|
||||
args = interruption.arguments or "(no arguments)"
|
||||
|
||||
approved = await prompt_yes_no(
|
||||
f"Agent {interruption.agent.name} wants to call '{tool_name}' with {args}. Approve?"
|
||||
)
|
||||
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
print("Approved tool call.")
|
||||
else:
|
||||
state.reject(interruption)
|
||||
print("Rejected tool call.")
|
||||
|
||||
# Resume the run with the updated state
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
|
||||
# Display the response
|
||||
reply = result.final_output or "[No final output produced]"
|
||||
print(f"Assistant: {reply}\n")
|
||||
if auto_mode:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Example demonstrating MongoDB session memory with a shared AsyncMongoClient.
|
||||
|
||||
In production you should create one AsyncMongoClient and pass it to all sessions
|
||||
so they share the same connection pool.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from pymongo.asynchronous.mongo_client import AsyncMongoClient
|
||||
|
||||
from agents import Agent, Runner
|
||||
from agents.extensions.memory import MongoDBSession
|
||||
|
||||
MONGO_URI = "mongodb://localhost:27017"
|
||||
DATABASE = "agents_example"
|
||||
|
||||
|
||||
async def main():
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# One client shared across all sessions (production pattern).
|
||||
client: AsyncMongoClient[Any] = AsyncMongoClient(MONGO_URI)
|
||||
|
||||
try:
|
||||
await client.admin.command("ping")
|
||||
except Exception:
|
||||
print("MongoDB is not available on localhost:27017")
|
||||
print("Start it with: docker run -d -p 27017:27017 mongo")
|
||||
return
|
||||
|
||||
session_a = MongoDBSession("conversation_a", client=client, database=DATABASE)
|
||||
session_b = MongoDBSession("conversation_b", client=client, database=DATABASE)
|
||||
|
||||
# Clean slate for the demo.
|
||||
await session_a.clear_session()
|
||||
await session_b.clear_session()
|
||||
|
||||
# --- Session A: multi-turn conversation ---
|
||||
print("=== Session A ===")
|
||||
result = await Runner.run(agent, "What city is the Golden Gate Bridge in?", session=session_a)
|
||||
print(f"Turn 1: {result.final_output}")
|
||||
|
||||
result = await Runner.run(agent, "What state is it in?", session=session_a)
|
||||
print(f"Turn 2: {result.final_output}")
|
||||
|
||||
result = await Runner.run(agent, "What's the population of that state?", session=session_a)
|
||||
print(f"Turn 3: {result.final_output}")
|
||||
|
||||
# --- Session B: independent conversation on the same client ---
|
||||
print("\n=== Session B ===")
|
||||
result = await Runner.run(agent, "What is the capital of France?", session=session_b)
|
||||
print(f"Turn 1: {result.final_output}")
|
||||
|
||||
# Show isolation.
|
||||
a_items = await session_a.get_items()
|
||||
b_items = await session_b.get_items()
|
||||
print(f"\nSession A items: {len(a_items)}, Session B items: {len(b_items)}")
|
||||
|
||||
# Cleanup.
|
||||
await session_a.clear_session()
|
||||
await session_b.clear_session()
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pip install "openai-agents[mongodb]"
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Example demonstrating session memory functionality.
|
||||
|
||||
This example shows how to use session memory to maintain conversation history
|
||||
across multiple agent runs without manually handling .to_input_list().
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, OpenAIConversationsSession, Runner
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create a session instance that will persist across runs
|
||||
session = OpenAIConversationsSession()
|
||||
|
||||
print("=== Session Example ===")
|
||||
print("The agent will remember previous messages automatically.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print("Sessions automatically handles conversation history.")
|
||||
|
||||
# Demonstrate the limit parameter - get only the latest 2 items
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
# print(latest_items)
|
||||
print("Latest 2 items:")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nFetched {len(latest_items)} out of total conversation history.")
|
||||
|
||||
# Get all items to show the difference
|
||||
all_items = await session.get_items()
|
||||
# print(all_items)
|
||||
print(f"Total items in session: {len(all_items)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Example demonstrating OpenAI Conversations session with human-in-the-loop (HITL) tool approval.
|
||||
|
||||
This example shows how to use OpenAI Conversations session memory combined with
|
||||
human-in-the-loop tool approval. The session maintains conversation history while
|
||||
requiring approval for specific tool calls.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, OpenAIConversationsSession, Runner, function_tool
|
||||
from examples.auto_mode import confirm_with_fallback, input_with_fallback, is_auto_mode
|
||||
|
||||
|
||||
async def _needs_approval(_ctx, _params, _call_id) -> bool:
|
||||
"""Always require approval for weather tool."""
|
||||
return True
|
||||
|
||||
|
||||
@function_tool(needs_approval=_needs_approval)
|
||||
def get_weather(location: str) -> str:
|
||||
"""Get weather for a location.
|
||||
|
||||
Args:
|
||||
location: The location to get weather for
|
||||
|
||||
Returns:
|
||||
Weather information as a string
|
||||
"""
|
||||
# Simulated weather data
|
||||
weather_data = {
|
||||
"san francisco": "Foggy, 58°F",
|
||||
"oakland": "Sunny, 72°F",
|
||||
"new york": "Rainy, 65°F",
|
||||
}
|
||||
# Check if any city name is in the provided location string
|
||||
location_lower = location.lower()
|
||||
for city, weather in weather_data.items():
|
||||
if city in location_lower:
|
||||
return weather
|
||||
return f"Weather data not available for {location}"
|
||||
|
||||
|
||||
async def prompt_yes_no(question: str) -> bool:
|
||||
"""Prompt user for yes/no answer.
|
||||
|
||||
Args:
|
||||
question: The question to ask
|
||||
|
||||
Returns:
|
||||
True if user answered yes, False otherwise
|
||||
"""
|
||||
return confirm_with_fallback(f"\n{question} (y/n): ", default=True)
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent with a tool that requires approval
|
||||
agent = Agent(
|
||||
name="HITL Assistant",
|
||||
instructions="You help users with information. Always use available tools when appropriate. Keep responses concise.",
|
||||
tools=[get_weather],
|
||||
)
|
||||
|
||||
# Create a session instance that will persist across runs
|
||||
session = OpenAIConversationsSession()
|
||||
|
||||
print("=== OpenAI Session + HITL Example ===")
|
||||
print("Enter a message to chat with the agent. Submit an empty line to exit.")
|
||||
print("The agent will ask for approval before using tools.\n")
|
||||
|
||||
auto_mode = is_auto_mode()
|
||||
|
||||
while True:
|
||||
# Get user input
|
||||
if auto_mode:
|
||||
user_message = input_with_fallback("You: ", "What's the weather in Oakland?")
|
||||
else:
|
||||
print("You: ", end="", flush=True)
|
||||
loop = asyncio.get_event_loop()
|
||||
user_message = await loop.run_in_executor(None, input)
|
||||
|
||||
if not user_message.strip():
|
||||
break
|
||||
|
||||
# Run the agent
|
||||
result = await Runner.run(agent, user_message, session=session)
|
||||
|
||||
# Handle interruptions (tool approvals)
|
||||
while result.interruptions:
|
||||
# Get the run state
|
||||
state = result.to_state()
|
||||
|
||||
for interruption in result.interruptions:
|
||||
tool_name = interruption.name or "Unknown tool"
|
||||
args = interruption.arguments or "(no arguments)"
|
||||
|
||||
approved = await prompt_yes_no(
|
||||
f"Agent {interruption.agent.name} wants to call '{tool_name}' with {args}. Approve?"
|
||||
)
|
||||
|
||||
if approved:
|
||||
state.approve(interruption)
|
||||
print("Approved tool call.")
|
||||
else:
|
||||
state.reject(interruption)
|
||||
print("Rejected tool call.")
|
||||
|
||||
# Resume the run with the updated state
|
||||
result = await Runner.run(agent, state, session=session)
|
||||
|
||||
# Display the response
|
||||
reply = result.final_output or "[No final output produced]"
|
||||
print(f"Assistant: {reply}\n")
|
||||
if auto_mode:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Example demonstrating Redis session memory functionality.
|
||||
|
||||
This example shows how to use Redis-backed session memory to maintain conversation
|
||||
history across multiple agent runs with persistence and scalability.
|
||||
|
||||
Note: This example clears the session at the start to ensure a clean demonstration.
|
||||
In production, you may want to preserve existing conversation history.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agents import Agent, Runner
|
||||
from agents.extensions.memory import RedisSession
|
||||
|
||||
DEFAULT_REDIS_URL = "redis://localhost:6379/0"
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
print("=== Redis Session Example ===")
|
||||
redis_url = os.environ.get("REDIS_URL", DEFAULT_REDIS_URL)
|
||||
print(f"This example uses Redis at {redis_url}")
|
||||
print("Set REDIS_URL to use a different Redis server.")
|
||||
print()
|
||||
|
||||
# Create a Redis session instance
|
||||
session_id = "redis_conversation_123"
|
||||
try:
|
||||
session = RedisSession.from_url(
|
||||
session_id,
|
||||
url=redis_url,
|
||||
)
|
||||
|
||||
# Test Redis connectivity
|
||||
if not await session.ping():
|
||||
print("Redis server is not available!")
|
||||
print("Please start Redis server and try again.")
|
||||
return
|
||||
|
||||
print("Connected to Redis successfully!")
|
||||
print(f"Session ID: {session_id}")
|
||||
|
||||
# Clear any existing session data for a clean start
|
||||
await session.clear_session()
|
||||
print("Session cleared for clean demonstration.")
|
||||
print("The agent will remember previous messages automatically.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print("Redis session automatically handles conversation history with persistence.")
|
||||
|
||||
# Demonstrate session persistence
|
||||
print("\n=== Session Persistence Demo ===")
|
||||
all_items = await session.get_items()
|
||||
print(f"Total messages stored in Redis: {len(all_items)}")
|
||||
|
||||
# Demonstrate the limit parameter
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
print("Latest 2 items:")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
# Demonstrate session isolation with a new session
|
||||
print("\n=== Session Isolation Demo ===")
|
||||
new_session = RedisSession.from_url(
|
||||
"different_conversation_456",
|
||||
url=redis_url,
|
||||
)
|
||||
|
||||
print("Creating a new session with different ID...")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"Hello, this is a new conversation!",
|
||||
session=new_session,
|
||||
)
|
||||
print(f"New session response: {result.final_output}")
|
||||
|
||||
# Show that sessions are isolated
|
||||
original_items = await session.get_items()
|
||||
new_items = await new_session.get_items()
|
||||
print(f"Original session has {len(original_items)} items")
|
||||
print(f"New session has {len(new_items)} items")
|
||||
print("Sessions are completely isolated!")
|
||||
|
||||
# Clean up the new session
|
||||
await new_session.clear_session()
|
||||
await new_session.close()
|
||||
|
||||
# Optional: Demonstrate TTL (time-to-live) functionality
|
||||
print("\n=== TTL Demo ===")
|
||||
ttl_session = RedisSession.from_url(
|
||||
"ttl_demo_session",
|
||||
url=redis_url,
|
||||
ttl=3600, # 1 hour TTL
|
||||
)
|
||||
|
||||
await Runner.run(
|
||||
agent,
|
||||
"This message will expire in 1 hour",
|
||||
session=ttl_session,
|
||||
)
|
||||
print("Created session with 1-hour TTL - messages will auto-expire")
|
||||
|
||||
await ttl_session.close()
|
||||
|
||||
# Close the main session
|
||||
await session.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print(f"Make sure Redis is running and reachable at {redis_url}")
|
||||
|
||||
|
||||
async def demonstrate_advanced_features():
|
||||
"""Demonstrate advanced Redis session features."""
|
||||
print("\n=== Advanced Features Demo ===")
|
||||
|
||||
# Custom key prefix for multi-tenancy
|
||||
tenant_session = RedisSession.from_url(
|
||||
"user_123",
|
||||
url=os.environ.get("REDIS_URL", DEFAULT_REDIS_URL),
|
||||
key_prefix="tenant_abc:sessions", # Custom prefix for isolation
|
||||
)
|
||||
|
||||
try:
|
||||
if await tenant_session.ping():
|
||||
print("Custom key prefix demo:")
|
||||
await Runner.run(
|
||||
Agent(name="Support", instructions="Be helpful"),
|
||||
"Hello from tenant ABC",
|
||||
session=tenant_session,
|
||||
)
|
||||
print("Session with custom key prefix created successfully")
|
||||
|
||||
await tenant_session.close()
|
||||
except Exception as e:
|
||||
print(f"Advanced features error: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(demonstrate_advanced_features())
|
||||
@@ -0,0 +1,78 @@
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner
|
||||
from agents.extensions.memory.sqlalchemy_session import SQLAlchemySession
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create a session instance with a session ID.
|
||||
# This example uses an in-memory SQLite database.
|
||||
# The `create_tables=True` flag is useful for development and testing.
|
||||
session = SQLAlchemySession.from_url(
|
||||
"conversation_123",
|
||||
url="sqlite+aiosqlite:///:memory:",
|
||||
create_tables=True,
|
||||
)
|
||||
|
||||
print("=== Session Example ===")
|
||||
print("The agent will remember previous messages automatically.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print("Sessions automatically handles conversation history.")
|
||||
|
||||
# Demonstrate the limit parameter - get only the latest 2 items
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
print("Latest 2 items:")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nFetched {len(latest_items)} out of total conversation history.")
|
||||
|
||||
# Get all items to show the difference
|
||||
all_items = await session.get_items()
|
||||
print(f"Total items in session: {len(all_items)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# To run this example, you need to install the sqlalchemy extras:
|
||||
# pip install "agents[sqlalchemy]"
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Example demonstrating session memory functionality.
|
||||
|
||||
This example shows how to use session memory to maintain conversation history
|
||||
across multiple agent runs without manually handling .to_input_list().
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from agents import Agent, Runner, SQLiteSession
|
||||
|
||||
|
||||
async def main():
|
||||
# Create an agent
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
instructions="Reply very concisely.",
|
||||
)
|
||||
|
||||
# Create a session instance that will persist across runs
|
||||
session_id = "conversation_123"
|
||||
session = SQLiteSession(session_id)
|
||||
|
||||
print("=== Session Example ===")
|
||||
print("The agent will remember previous messages automatically.\n")
|
||||
|
||||
# First turn
|
||||
print("First turn:")
|
||||
print("User: What city is the Golden Gate Bridge in?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What city is the Golden Gate Bridge in?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Second turn - the agent will remember the previous conversation
|
||||
print("Second turn:")
|
||||
print("User: What state is it in?")
|
||||
result = await Runner.run(agent, "What state is it in?", session=session)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
# Third turn - continuing the conversation
|
||||
print("Third turn:")
|
||||
print("User: What's the population of that state?")
|
||||
result = await Runner.run(
|
||||
agent,
|
||||
"What's the population of that state?",
|
||||
session=session,
|
||||
)
|
||||
print(f"Assistant: {result.final_output}")
|
||||
print()
|
||||
|
||||
print("=== Conversation Complete ===")
|
||||
print("Notice how the agent remembered the context from previous turns!")
|
||||
print("Sessions automatically handles conversation history.")
|
||||
|
||||
# Demonstrate the limit parameter - get only the latest 2 items
|
||||
print("\n=== Latest Items Demo ===")
|
||||
latest_items = await session.get_items(limit=2)
|
||||
print("Latest 2 items:")
|
||||
for i, msg in enumerate(latest_items, 1):
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
print(f" {i}. {role}: {content}")
|
||||
|
||||
print(f"\nFetched {len(latest_items)} out of total conversation history.")
|
||||
|
||||
# Get all items to show the difference
|
||||
all_items = await session.get_items()
|
||||
print(f"Total items in session: {len(all_items)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user