chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:17 +08:00
commit 4ed4e9ff99
1368 changed files with 334957 additions and 0 deletions
@@ -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())
+586
View File
@@ -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())
+152
View File
@@ -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())
+124
View File
@@ -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")
+405
View File
@@ -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())
+78
View File
@@ -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())
+181
View File
@@ -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())
+77
View File
@@ -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())