Markdown Converter
Agent skill for markdown-converter
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Sign in to like and favorite skills
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a 90-minute workshop repository teaching developers how to build durable AI agents using OpenAI Agents SDK + Temporal. The workshop is designed to run in GitHub Codespaces with zero local setup required.
OpenAI Agents SDK is critical to this entire workshop. All LLM tool calls must happen using this SDK.
All tool calls have to be real, no mocking. Use the weather API. All code uses asyncio for efficient I/O operations.
Target Audience: Beginner-intermediate Python developers Workshop Format: 30 minutes instruction + 4×15 minute hands-on exercises (using complete solutions)
IMPORTANT: This workshop uses a "learn by exploring" approach:
solutions/ directoryexercises/ directory as optional homeworksolutions/ directory contains fully working code that students will run, explore, and learn fromexercises/ directory contains starter code with TODO markers for independent practicerich, typer, pytest, ruff, mypymake setup # Install dependencies and set up environment make env # Validate environment variables (OPENAI_API_KEY)
make lint # Run ruff linter make test # Run pytest test suite
temporal server start-dev # Start Temporal local dev server (idempotent)
.ipynb) with all necessary codeThe workshop follows a progressive learning path:
Exercise 1: Basic Agent Pattern
User Query 👤 ↓ Agent (OpenAI LLM) 🤖 ↓ Tool Function 🔧 ↓ External API 🌐 ↓ Data returned to Agent 📊 ↓ Agent uses LLM to generate response 💬 ↓ Return to user ✅
Exercise 2: Temporal Fundamentals
Workflow Request 👤 ↓ Temporal Workflow 🎭 ↓ Temporal Activity ⚙️ ↓ Result ✅
Exercise 3: Durable Agent (Integration)
User Query 👤 ↓ Temporal Workflow (orchestration layer) 🎭 ↓ Activity: Call LLM with tools 🤖 ↓ [If tool needed] Activity: Execute tool 🔧 ↓ External API Call 🌐 ↓ Data returned to Agent 📊 ↓ Agent leverages LLM to generate response 💬 ↓ Return to user ✅
Exercise 4: Multi-Agent Routing
User Query 👤 ↓ Temporal Workflow 🎭 ↓ Activity: Triage Agent 🔍 ↓ Activity: Handoff to Specialist 🔀 ↓ Activity: Specialist Agent 💬 ↓ Return to user ✅
Exercise 1 - Agent Hello World (Workshop:
solutions/01_agent_hello_world/, Homework: exercises/01_agent_hello_world/)
@function_tool decorator for custom tools# Example from Exercise 1 import asyncio import httpx from agents import Agent, Runner, function_tool @function_tool async def get_weather_alerts(state: str) -> str: """Get weather alerts for a US state from NWS.""" url = f"https://api.weather.gov/alerts/active/area/{state.upper()}" headers = {"User-Agent": "OpenAI-Agents-Workshop"} async with httpx.AsyncClient() as client: response = await client.get(url, headers=headers, timeout=10.0) data = response.json() features = data.get("features", []) if not features: return f"No active weather alerts for {state.upper()}." # Format alerts alerts = [f.get("properties", {}).get("event") for f in features[:5]] return f"Active alerts: {', '.join(alerts)}" agent = Agent( name="Weather Agent", instructions="You help users get weather alerts for US states.", tools=[get_weather_alerts] ) async def main(): result = await Runner.run(agent, "Are there weather alerts for CA?") print(result.final_output) if __name__ == "__main__": asyncio.run(main())
@function_toolexercise.ipynb)Exercise 2 - Temporal Hello World (Workshop:
solutions/02_temporal_hello_world/, Homework: exercises/02_temporal_hello_world/)
Exercise 3 - Durable Agent (Workshop:
solutions/03_durable_agent/, Homework: exercises/03_durable_agent/)
activity_as_tool() patternawait Runner().run(), async def activities and workflowsExercise 3 follows the production-ready structure from temporal-weather-openai-agents:
import httpx from temporalio import activity @activity.defn(name="get_weather") async def get_weather(state: str) -> dict: """Fetch active NWS alerts for a 2-letter US state code.""" headers = {"User-Agent": "Temporal-Agents-Workshop/1.0"} async with httpx.AsyncClient(timeout=10) as client: r = await client.get( f"https://api.weather.gov/alerts/active/area/{state}", headers=headers ) r.raise_for_status() data = r.json() alerts = [] for f in (data.get("features") or [])[:5]: p = f.get("properties", {}) alerts.append({ "event": p.get("event"), "headline": p.get("headline"), "severity": p.get("severity"), "area": p.get("areaDesc"), }) return {"state": state.upper(), "count": len(alerts), "alerts": alerts}
from datetime import timedelta from temporalio import workflow from temporalio.contrib import openai_agents from agents import Agent, Runner TASK_QUEUE = "weather-agents" @workflow.defn class WeatherAgentWorkflow: @workflow.run async def run(self, user_query: str) -> str: agent = Agent( name="Weather Assistant", instructions="You are a helpful assistant that explains current weather alerts for U.S. states.", tools=[ openai_agents.workflow.activity_as_tool( get_weather, start_to_close_timeout=timedelta(seconds=10), ) ], ) # Note: Runner() is instantiated, not just Runner result = await Runner().run(agent, user_query) return getattr(result, "final_output", str(result))
from temporalio.client import Client from temporalio.worker import Worker from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions async def main(): client = await Client.connect( "localhost:7233", plugins=[ OpenAIAgentsPlugin( model_params=ModelActivityParameters( start_to_close_timeout=timedelta(seconds=30) ) ) ], ) worker = Worker( client, task_queue=TASK_QUEUE, workflows=[WeatherAgentWorkflow], activities=[get_weather], workflow_runner=SandboxedWorkflowRunner( restrictions=SandboxRestrictions.default.with_passthrough_modules("httpx") ) ) await worker.run()
from temporalio.client import Client from temporalio.contrib.openai_agents import OpenAIAgentsPlugin async def main(): client = await Client.connect("localhost:7233", plugins=[OpenAIAgentsPlugin()]) # Use start_workflow, not execute_workflow handle = await client.start_workflow( WeatherAgentWorkflow.run, "What weather alerts are active in CA?", id="weather-workflow-id", task_queue=TASK_QUEUE ) print(f"🚀 Started workflow: {handle.id}") result = await handle.result() print(f"🤖 Response: {result}")
Key Benefits:
with_passthrough_modules()Important Notes:
dict not string - LLM interprets structured datauser_query (no trace_id)Runner() is instantiated (not just Runner)start_workflow (not execute_workflow)solutions/04_agent_routing/, Homework: exercises/04_agent_routing/)
# Example from Exercise 4 - Agent Definitions from agents import Agent def french_agent() -> Agent: """Agent specialized in French language communication.""" return Agent( name="French Agent", instructions="You only speak French. Respond naturally in French.", model="gpt-4" # OpenAI model ) def triage_agent() -> Agent: """Triage agent that routes to language specialists.""" return Agent( name="Triage Agent", instructions="Detect language and handoff to appropriate specialist.", handoffs=[french_agent(), spanish_agent(), english_agent()], model="gpt-4" ) # Workflow Implementation @workflow.defn class RoutingWorkflow: @workflow.run async def run(self, user_query: str) -> str: """Execute routing workflow with language detection and handoff.""" config = RunConfig() new_message: ChatCompletionMessageParam = { "role": "user", "content": user_query, } # Triage agent analyzes and routes to specialist result = await Runner.run(triage_agent(), new_message, config=config) return result.final_output
Key Differences from Other Exercises:
python worker.py (terminal 1) + python starter.py (terminal 2)solutions/ # 👈 Primary workshop materials - complete implementations exercises/ # 👈 Optional homework - starter code with TODOs slides/ # Workshop presentation materials scripts/ # Bootstrap, environment checks, Temporal startup .devcontainer/ # Codespaces configuration
Each exercise directory contains:
.ipynb) for interactive learning
solutions/: Complete, well-commented implementations for workshopexercises/: Starter code with TODO markers for homeworksolutions/04_agent_routing/: Complete implementationexercises/04_agent_routing/: Starter code with TODOsREADME.md in each directory with: Goal, Steps (≤5), Expected Output, Stretch Goal, TimeboxIn Exercise 3, LLM calls are wrapped in Temporal activities rather than called directly in workflows. This provides:
All workflows in this repository follow a consistent naming pattern for workflow IDs:
Pattern:
{prefix}-{day}-{month}-{date}-{time}est
Example:
durable-agent-wed-oct-16-094832est
Implementation:
from datetime import datetime import pytz # Generate workflow ID with EST timestamp est = pytz.timezone('US/Eastern') now = datetime.now(est) workflow_id = f"durable-agent-{now.strftime('%a-%b-%d-%I%M%S').lower()}est"
Benefits:
Examples by Exercise:
hello-workflow-thu-oct-16-095919estdurable-agent-wed-oct-16-094832estrouting-fri-oct-17-103045estRequired Dependencies:
pytz to pip install commands: %pip install --quiet temporalio openai-agents httpx rich nest-asyncio pytzfrom datetime import datetime and import pytzThe durable agent implementation connects two observability systems:
trace_id: Correlation key printed to link both systemsAll code follows these principles:
OPENAI_API_KEY - Required for exercises 1, 3, 4.env.sample provides template (never commit .env)scripts/check_env.py validates environment before exercises run.devcontainer/scripts/bootstrap.sh runs automatically
scripts/run_temporal.sh checks if Temporal is already running. If yes, exits cleanly without error.
/solutions/ and /exercises/make env output for missing environment variablesmake temporal-uptrace_id for correlationGitHub Actions workflow (
.github/workflows/ci.yml):
ruff → mypy → pytest -q