diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6ebf1386..a2fcae5d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,6 +1,6 @@ # This workflow is triggered when a GitHub release is created. # It can also be run manually to re-publish to PyPI in case it failed for some reason. -# You can run this workflow by navigating to https://www.github.com/scaleapi/agentex-python/actions/workflows/publish-pypi.yml +# You can run this workflow by navigating to https://www.github.com/scaleapi/scale-agentex-python/actions/workflows/publish-pypi.yml name: Publish PyPI on: workflow_dispatch: diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 0521b7f6..321914dd 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -9,7 +9,7 @@ jobs: release_doctor: name: release doctor runs-on: ubuntu-latest - if: github.repository == 'scaleapi/agentex-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + if: github.repository == 'scaleapi/scale-agentex-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - uses: actions/checkout@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d04f223f..383dd5a3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.1" + ".": "0.5.2" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e50822b4..23c0d3e9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 34 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-2b422fbf02ff3b77795fb8c71cbe784de3a3add48560655ba4fe7f3fcc509995.yml -openapi_spec_hash: bca5c04d823694c87417dae188480291 -config_hash: 6481ea6b42040f435dedcb00a98f35f8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp%2Fagentex-sdk-2ba36d013a2829080b1003d4fecc30e89447db013f87491dd4d76f728d200b85.yml +openapi_spec_hash: ff4de1c4bd38d4ff35bcf30755f0d870 +config_hash: 0197f86ba1a4b1b5ce813d0e62138588 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8a4af5..3b4c5368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ # Changelog -## 0.5.1 (2025-10-29) +## 0.5.2 (2025-10-31) -Full Changelog: [v0.5.0...v0.5.1](https://github.com/scaleapi/agentex-python/compare/v0.5.0...v0.5.1) +Full Changelog: [v0.5.0...v0.5.2](https://github.com/scaleapi/scale-agentex-python/compare/v0.5.0...v0.5.2) -### Bug Fixes +### Features + +* **api:** manual updates ([dc66b57](https://github.com/scaleapi/scale-agentex-python/commit/dc66b57618525669b3aa15676343ef542675a5f9)) +* bump the helm chart version ([1ffafb0](https://github.com/scaleapi/scale-agentex-python/commit/1ffafb0406138d6abd84254fa394b88c4a28ce70)) + + +### Chores -* **client:** close streams without requiring full consumption ([f56acae](https://github.com/scaleapi/agentex-python/commit/f56acae74ee83a116e735ca7bf68f2096aafaf6e)) +* sync repo ([0e05416](https://github.com/scaleapi/scale-agentex-python/commit/0e05416219ca93ae347e6175804bc0f2259a6b44)) ## 0.5.0 (2025-10-28) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04c64123..16fcfe65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,7 +62,7 @@ If you’d like to use the repository from source, you can either install from g To install via git: ```sh -$ pip install git+ssh://git@github.com/scaleapi/agentex-python.git +$ pip install git+ssh://git@github.com/scaleapi/scale-agentex-python.git ``` Alternatively, you can build from source and install the wheel file: @@ -120,7 +120,7 @@ the changes aren't made through the automated pipeline, you may want to make rel ### Publish with a GitHub workflow -You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/scaleapi/agentex-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/scaleapi/scale-agentex-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. ### Publish manually diff --git a/README.md b/README.md index 50ff2e44..8d2c7f36 100644 --- a/README.md +++ b/README.md @@ -268,9 +268,9 @@ task = response.parse() # get the object that `tasks.list()` would have returne print(task) ``` -These methods return an [`APIResponse`](https://github.com/scaleapi/agentex-python/tree/main/src/agentex/_response.py) object. +These methods return an [`APIResponse`](https://github.com/scaleapi/scale-agentex-python/tree/main/src/agentex/_response.py) object. -The async client returns an [`AsyncAPIResponse`](https://github.com/scaleapi/agentex-python/tree/main/src/agentex/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. +The async client returns an [`AsyncAPIResponse`](https://github.com/scaleapi/scale-agentex-python/tree/main/src/agentex/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` @@ -374,7 +374,7 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/scaleapi/agentex-python/issues) with questions, bugs, or suggestions. +We are keen for your feedback; please open an [issue](https://www.github.com/scaleapi/scale-agentex-python/issues) with questions, bugs, or suggestions. ### Determining the installed version diff --git a/examples/tutorials/00_sync/000_hello_acp/README.md b/examples/tutorials/00_sync/000_hello_acp/README.md index 4d0ed634..944e46a5 100644 --- a/examples/tutorials/00_sync/000_hello_acp/README.md +++ b/examples/tutorials/00_sync/000_hello_acp/README.md @@ -3,10 +3,7 @@ This is a simple AgentEx agent that just says hello and acknowledges the user's message to show which ACP methods need to be implemented for the sync ACP type. The simplest agent type: synchronous request/response pattern with a single `@acp.on_message_send` handler. Best for stateless operations that complete immediately. -## What You'll Learn -- Building a basic synchronous agent -- The `@acp.on_message_send` handler pattern -- When to use sync vs agentic agents +## Official Documentation ## Prerequisites - Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) @@ -42,3 +39,4 @@ That's it - one handler, immediate response. No task creation, no state manageme Sync agents are the simplest way to get started with AgentEx. They're perfect for learning the basics and building stateless agents. Once you need conversation memory or task tracking, you'll graduate to agentic agents. **Next:** [010_multiturn](../010_multiturn/) - Add conversation memory to your agent +[000 Hello ACP](https://dev.agentex.scale.com/docs/tutorials/sync/000_hello_acp) diff --git a/examples/tutorials/00_sync/010_multiturn/README.md b/examples/tutorials/00_sync/010_multiturn/README.md index 92abab5f..e095927d 100644 --- a/examples/tutorials/00_sync/010_multiturn/README.md +++ b/examples/tutorials/00_sync/010_multiturn/README.md @@ -1,54 +1,7 @@ # [Sync] Multiturn -Handle multi-turn conversations in synchronous agents by manually maintaining conversation history and context between messages. +This tutorial demonstrates how to handle multiturn conversations in AgentEx agents using the Agent 2 Client Protocol (ACP). -## What You'll Learn -- How to handle conversation history in sync agents -- Building context from previous messages -- The limitations of stateless multiturn patterns +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of basic sync agents (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/00_sync/010_multiturn -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -Sync agents are stateless by default. To handle multi-turn conversations, you need to: -1. Accept conversation history in the request -2. Maintain context across messages -3. Return responses that build on previous exchanges - -```python -@acp.on_message_send -async def handle_message_send(params: SendMessageParams): - # Accept conversation history from client - history = params.conversation_history - - # Build context from history - context = build_context(history) - - # Generate response considering full context - response = generate_response(params.content, context) - - return TextContent(author="agent", content=response) -``` - -The handler accepts history, builds context, and returns responses that reference previous exchanges. - -## When to Use -- Simple chatbots that need conversation memory -- When client can maintain and send conversation history -- Quick prototypes before building full agentic agents - -## Why This Matters -While sync agents can handle conversations, you're responsible for managing state on the client side. This becomes complex quickly. For production conversational agents, consider agentic agents ([10_agentic/00_base/010_multiturn](../../10_agentic/00_base/010_multiturn/)) where the platform manages state automatically. - -**Next:** [020_streaming](../020_streaming/) - Stream responses in real-time +[010 Multiturn](https://dev.agentex.scale.com/docs/tutorials/sync/010_multiturn) \ No newline at end of file diff --git a/examples/tutorials/00_sync/020_streaming/README.md b/examples/tutorials/00_sync/020_streaming/README.md index 0204bf37..b936628f 100644 --- a/examples/tutorials/00_sync/020_streaming/README.md +++ b/examples/tutorials/00_sync/020_streaming/README.md @@ -1,45 +1,9 @@ # [Sync] Streaming -Stream responses progressively using async generators instead of returning a single message. Enables showing partial results as they're generated. +This tutorial demonstrates how to implement streaming responses in AgentEx agents using the Agent 2 Client Protocol (ACP). -## What You'll Learn -- How to stream responses using async generators -- The `yield` pattern for progressive updates -- When streaming improves user experience +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of basic sync agents (see [000_hello_acp](../000_hello_acp/)) +[020 Streaming](https://dev.agentex.scale.com/docs/tutorials/sync/020_streaming) -## Quick Start -```bash -cd examples/tutorials/00_sync/020_streaming -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Code - -```python -@acp.on_message_send -async def handle_message_send(params: SendMessageParams): - async def stream_response(): - for chunk in response_chunks: - yield TaskMessageUpdate(content=TextContent(...)) - - return stream_response() -``` - -Return an async generator instead of a single response - each `yield` sends an update to the client. - -## When to Use -- Streaming LLM responses (OpenAI, Anthropic, etc.) -- Large data processing with progress updates -- Any operation that takes >1 second to complete -- Improving perceived responsiveness - -## Why This Matters -Streaming dramatically improves user experience for longer operations. Instead of waiting 10 seconds for a complete response, users see results immediately as they're generated. This is essential for modern AI agents. - -**Next:** Ready for task management? → [10_agentic/00_base/000_hello_acp](../../10_agentic/00_base/000_hello_acp/) diff --git a/examples/tutorials/10_agentic/00_base/000_hello_acp/README.md b/examples/tutorials/10_agentic/00_base/000_hello_acp/README.md index 59bf95d6..36008586 100644 --- a/examples/tutorials/10_agentic/00_base/000_hello_acp/README.md +++ b/examples/tutorials/10_agentic/00_base/000_hello_acp/README.md @@ -1,49 +1,7 @@ # [Agentic] Hello ACP -Agentic agents use three handlers for async task management: `on_task_create`, `on_task_event_send`, and `on_task_cancel`. Unlike sync agents, tasks persist and can receive multiple events over time. +This tutorial demonstrates how to implement the base agentic ACP type in AgentEx agents. -## What You'll Learn -- The three-handler pattern for agentic agents -- How tasks differ from sync messages -- When to use agentic vs sync agents +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of sync agents (see [00_sync/000_hello_acp](../../../00_sync/000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/000_hello_acp -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -```python -@acp.on_task_create -async def handle_task_create(params: CreateTaskParams): - # Initialize task state, send welcome message - -@acp.on_task_event_send -async def handle_event_send(params: SendEventParams): - # Handle each message/event in the task - -@acp.on_task_cancel -async def handle_task_cancel(params: CancelTaskParams): - # Cleanup when task is cancelled -``` - -Three handlers instead of one, giving you full control over task lifecycle. Tasks can receive multiple events and maintain state across them. - -## When to Use -- Conversational agents that need memory -- Operations that require task tracking -- Agents that need lifecycle management (initialization, cleanup) -- Building towards production systems - -## Why This Matters -The task-based model is the foundation of production agents. Unlike sync agents where each message is independent, agentic agents maintain persistent tasks that can receive multiple events, store state, and have full lifecycle management. This is the stepping stone to Temporal-based agents. - -**Next:** [010_multiturn](../010_multiturn/) - Add conversation memory +[000 Hello Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/hello_acp/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/00_base/010_multiturn/README.md b/examples/tutorials/10_agentic/00_base/010_multiturn/README.md index 6c7b3a4f..40eb8d84 100644 --- a/examples/tutorials/10_agentic/00_base/010_multiturn/README.md +++ b/examples/tutorials/10_agentic/00_base/010_multiturn/README.md @@ -1,61 +1,7 @@ # [Agentic] Multiturn -Handle multi-turn conversations in agentic agents with task-based state management. Each task maintains its own conversation history automatically. +This tutorial demonstrates how to handle multiturn conversations in AgentEx agents using the agentic ACP type. -## What You'll Learn -- How tasks maintain conversation state across multiple exchanges -- Difference between sync and agentic multiturn patterns -- Building stateful conversational agents with minimal code +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of basic agentic agents (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/010_multiturn -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -Unlike sync agents where you manually track conversation history, agentic agents automatically maintain state within each task: - -```python -@app.on_task_event_send() -async def on_task_event_send(event_send: TaskEventSendInput): - # The task's messages list automatically includes all previous exchanges - messages = event_send.task.messages - - # No need to manually pass history - it's already there! - response = await openai_client.chat.completions.create( - model="gpt-4o-mini", - messages=messages - ) - - return {"content": response.choices[0].message.content} -``` - -## Try It - -1. Start the agent with the command above -2. Open the web UI or use the notebook to create a task -3. Send multiple messages in the same task: - - "What's 25 + 17?" - - "What was that number again?" - - "Multiply it by 2" -4. Notice the agent remembers context from previous exchanges - -## When to Use -- Conversational agents that need memory across exchanges -- Chat interfaces where users ask follow-up questions -- Agents that build context over time within a session - -## Why This Matters -Task-based state management eliminates the complexity of manually tracking conversation history. The AgentEx platform handles state persistence automatically, making it easier to build stateful agents without custom session management code. - -**Comparison:** In the sync version ([00_sync/010_multiturn](../../../00_sync/010_multiturn/)), you manually manage conversation history. Here, the task object does it for you. - -**Next:** [020_streaming](../020_streaming/) - Add real-time streaming responses +[010 Multiturn Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/multiturn/) diff --git a/examples/tutorials/10_agentic/00_base/020_streaming/README.md b/examples/tutorials/10_agentic/00_base/020_streaming/README.md index 43026b42..bbd1d90b 100644 --- a/examples/tutorials/10_agentic/00_base/020_streaming/README.md +++ b/examples/tutorials/10_agentic/00_base/020_streaming/README.md @@ -1,47 +1,7 @@ # [Agentic] Streaming -Stream responses in agentic agents using `adk.messages.create()` to send progressive updates. More flexible than sync streaming since you can send multiple messages at any time. +This tutorial demonstrates how to implement streaming responses in AgentEx agents using the agentic ACP type. -## What You'll Learn -- How to stream with explicit message creation -- Difference between sync and agentic streaming patterns -- When to send multiple messages vs single streamed response +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of agentic basics (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/020_streaming -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -```python -@acp.on_task_event_send -async def handle_event_send(params: SendEventParams): - # Send first message - await adk.messages.create(task_id=task_id, content=...) - - # Do work... - - # Send second message - await adk.messages.create(task_id=task_id, content=...) -``` - -Unlike sync streaming (which uses async generators), agentic streaming uses explicit message creation calls, giving you more control over when and what to send. - -## When to Use -- Multi-step processes with intermediate results -- Long-running operations with progress updates -- Agents that need to send messages at arbitrary times -- More complex streaming patterns than simple LLM responses - -## Why This Matters -Agentic streaming is more powerful than sync streaming. You can send messages at any time, from anywhere in your code, and even from background tasks. This flexibility is essential for complex agents with multiple concurrent operations. - -**Next:** [030_tracing](../030_tracing/) - Add observability to your agents +[020 Streaming Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/streaming/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/00_base/030_tracing/README.md b/examples/tutorials/10_agentic/00_base/030_tracing/README.md index 8a67c5dc..ea41a8f8 100644 --- a/examples/tutorials/10_agentic/00_base/030_tracing/README.md +++ b/examples/tutorials/10_agentic/00_base/030_tracing/README.md @@ -1,53 +1,7 @@ # [Agentic] Tracing -Add observability to your agents with spans and traces using `adk.tracing.start_span()`. Track execution flow, measure performance, and debug complex agent behaviors. +This tutorial demonstrates how to implement hierarchical and custom tracing in AgentEx agents using the agentic ACP type. -## What You'll Learn -- How to instrument agents with tracing -- Creating hierarchical spans to track operations -- Viewing traces in Scale Groundplane -- Performance debugging with observability +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of agentic agents (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/030_tracing -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -```python -# Start a span to track an operation -span = await adk.tracing.start_span( - trace_id=task.id, - name="LLM Call", - input={"prompt": prompt} -) - -# Do work... - -# End span with output -await adk.tracing.end_span( - span_id=span.id, - output={"response": response} -) -``` - -Spans create a hierarchical view of agent execution, making it easy to see which operations take time and where errors occur. - -## When to Use -- Debugging complex agent behaviors -- Performance optimization and bottleneck identification -- Production monitoring and observability -- Understanding execution flow in multi-step agents - -## Why This Matters -Without tracing, debugging agents is like flying blind. Tracing gives you visibility into what your agent is doing, how long operations take, and where failures occur. It's essential for production agents and invaluable during development. - -**Next:** [040_other_sdks](../040_other_sdks/) - Integrate any SDK or framework +[030 Tracing Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/tracing/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/00_base/040_other_sdks/README.md b/examples/tutorials/10_agentic/00_base/040_other_sdks/README.md index 7234f3d6..8be83e82 100644 --- a/examples/tutorials/10_agentic/00_base/040_other_sdks/README.md +++ b/examples/tutorials/10_agentic/00_base/040_other_sdks/README.md @@ -1,45 +1,7 @@ # [Agentic] Other SDKs -Agents are just Python code - integrate any SDK you want (OpenAI, Anthropic, LangChain, LlamaIndex, custom libraries, etc.). AgentEx doesn't lock you into a specific framework. +This tutorial demonstrates how to use other SDKs in AgentEx agents to show the flexibility that agents are just code. -## What You'll Learn -- How to integrate OpenAI, Anthropic, or any SDK -- What AgentEx provides vs what you bring -- Framework-agnostic agent development -- Building agents with your preferred tools +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of agentic agents (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/040_other_sdks -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Insight - -AgentEx provides: -- ACP protocol implementation (task management, message handling) -- Deployment infrastructure -- Monitoring and observability - -You provide: -- Agent logic using whatever SDK/library you want -- Tools and capabilities specific to your use case - -Mix and match OpenAI, Anthropic, LangChain, or roll your own - it's all just Python. - -## When to Use -- You have an existing agent codebase to migrate -- Your team prefers specific SDKs or frameworks -- You need features from multiple providers -- You want full control over your agent logic - -## Why This Matters -AgentEx is infrastructure, not a framework. We handle deployment, task management, and protocol implementation - you handle the agent logic with whatever tools you prefer. This keeps you flexible and avoids vendor lock-in. - -**Next:** [080_batch_events](../080_batch_events/) - See when you need Temporal +[040 Other SDKs Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/other_sdks/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/00_base/080_batch_events/README.md b/examples/tutorials/10_agentic/00_base/080_batch_events/README.md index 7a2efce3..bddd3ed6 100644 --- a/examples/tutorials/10_agentic/00_base/080_batch_events/README.md +++ b/examples/tutorials/10_agentic/00_base/080_batch_events/README.md @@ -1,46 +1,7 @@ # [Agentic] Batch Events -Demonstrates limitations of the base agentic protocol with concurrent event processing. When multiple events arrive rapidly, base agentic agents handle them sequentially, which can cause issues. +This tutorial demonstrates batch event processing and the limitations of the base agentic ACP protocol. -## What You'll Learn -- Limitations of non-Temporal agentic agents -- Race conditions and ordering issues in concurrent scenarios -- When you need workflow orchestration -- Why this motivates Temporal adoption +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Understanding of agentic patterns (see previous tutorials) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/00_base/080_batch_events -uv run agentex agents run --manifest manifest.yaml -``` - -## Why This Matters - -This tutorial shows **when you need Temporal**. If your agent needs to: -- Handle events that might arrive out of order -- Process multiple events in parallel safely -- Maintain consistent state under concurrent load - -Then you should use Temporal workflows (see tutorials 10_agentic/10_temporal/) which provide: -- Deterministic event ordering -- Safe concurrent processing -- Guaranteed state consistency - -This is the "breaking point" tutorial that motivates moving to Temporal for production agents. - -## When to Use (This Pattern) -This tutorial shows what NOT to use for production. Use base agentic agents only when: -- Events are infrequent (< 1 per second) -- Order doesn't matter -- State consistency isn't critical - -## Why This Matters -Every production agent eventually hits concurrency issues. This tutorial shows you those limits early, so you know when to graduate to Temporal. Better to learn this lesson in a tutorial than in production! - -**Next:** Ready for production? → [../10_temporal/000_hello_acp](../../10_temporal/000_hello_acp/) or explore [090_multi_agent_non_temporal](../090_multi_agent_non_temporal/) for complex non-Temporal coordination +[080 Batch Events Base Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/base/batch_events/) diff --git a/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal/README.md b/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal/README.md index 476b75ee..2cba9e17 100644 --- a/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal/README.md +++ b/examples/tutorials/10_agentic/00_base/090_multi_agent_non_temporal/README.md @@ -45,11 +45,9 @@ The system uses a shared build configuration with type-safe interfaces: ## 🚀 Quick Start ### Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Python 3.12+ and uv package manager +- Python 3.12+ +- uv package manager - OpenAI API key (set `OPENAI_API_KEY` or create `.env` file) -- Understanding of agentic patterns (see previous tutorials) ### Running the System @@ -197,14 +195,3 @@ This tutorial demonstrates: - **AgentEx CLI usage** for development and deployment - **Inter-agent communication patterns** with proper error handling - **Scalable agent architecture** with clear separation of concerns - -## When to Use -- Complex workflows requiring multiple specialized agents -- Content pipelines with review/approval steps -- Systems where each stage needs different capabilities -- When you want agent separation without Temporal (though Temporal is recommended for production) - -## Why This Matters -This shows how far you can go with non-Temporal multi-agent systems. However, note the limitations: manual state management, potential race conditions, and no built-in durability. For production multi-agent systems, consider Temporal ([../10_temporal/](../../10_temporal/)) which provides workflow orchestration, durability, and state management out of the box. - -**Next:** Ready for production workflows? → [../../10_temporal/000_hello_acp](../../10_temporal/000_hello_acp/) diff --git a/examples/tutorials/10_agentic/10_temporal/000_hello_acp/README.md b/examples/tutorials/10_agentic/10_temporal/000_hello_acp/README.md index 6a5431f5..3cb435ec 100644 --- a/examples/tutorials/10_agentic/10_temporal/000_hello_acp/README.md +++ b/examples/tutorials/10_agentic/10_temporal/000_hello_acp/README.md @@ -1,55 +1,7 @@ -# [Temporal] Hello ACP +# [Agentic] Hello ACP with Temporal -Temporal workflows make agents durable - they survive restarts and can run indefinitely without consuming resources while idle. Instead of handlers, you define a workflow class with `@workflow.run` and `@workflow.signal` methods. +This tutorial demonstrates how to implement the agentic ACP type with Temporal workflows in AgentEx agents. -## What You'll Learn -- Building durable agents with Temporal workflows -- The workflow and signal pattern -- How workflows survive failures and resume automatically -- When to use Temporal vs base agentic agents +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root (includes Temporal) -- Temporal UI available at http://localhost:8233 -- Understanding of base agentic agents (see [../../00_base/080_batch_events](../../00_base/080_batch_events/) to understand why Temporal) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/10_temporal/000_hello_acp -uv run agentex agents run --manifest manifest.yaml -``` - -**Monitor:** Check Temporal UI at http://localhost:8233 to see your durable workflow running. - -## Key Pattern - -```python -@workflow.defn(name="my-workflow") -class MyWorkflow(BaseWorkflow): - @workflow.run - async def on_task_create(self, params: CreateTaskParams): - # Wait indefinitely for events - workflow stays alive - await workflow.wait_condition(lambda: self._complete) - - @workflow.signal(name=SignalName.RECEIVE_EVENT) - async def on_task_event_send(self, params: SendEventParams): - # Handle events as signals to the workflow -``` - -## When to Use -- Production agents that need guaranteed execution -- Long-running tasks (hours, days, weeks, or longer) -- Operations that must survive system failures -- Agents with concurrent event handling requirements -- When you need durability and observability - -## Why This Matters -**Without Temporal:** If your worker crashes, the agent loses all state and has to start over. - -**With Temporal:** The workflow resumes exactly where it left off. If it crashes mid-conversation, Temporal brings it back up with full context intact. Can run for years if needed, only consuming resources when actively processing. - -This is the foundation for production-ready agents that handle real-world reliability requirements. - -**Next:** [010_agent_chat](../010_agent_chat/) - Build a complete conversational agent with tools +[000 Hello Temporal Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/temporal/hello_acp/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/010_agent_chat/README.md b/examples/tutorials/10_agentic/10_temporal/010_agent_chat/README.md index 4707472c..3b809817 100644 --- a/examples/tutorials/10_agentic/10_temporal/010_agent_chat/README.md +++ b/examples/tutorials/10_agentic/10_temporal/010_agent_chat/README.md @@ -1,47 +1,7 @@ -# [Temporal] Agent Chat +# [Agentic] Agent Chat with Temporal -Combine streaming responses, multi-turn chat, tool calling, and tracing - all with Temporal's durability guarantees. This shows how to build a complete conversational agent that can survive failures. +This tutorial demonstrates how to implement streaming multiturn tool-enabled chat with tracing using Temporal workflows in AgentEx agents. -## What You'll Learn -- Building a complete conversational agent with Temporal -- Combining streaming, multiturn, tools, and tracing -- How all agent capabilities work together with durability -- Production-ready conversational patterns +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- Understanding of Temporal basics (see [000_hello_acp](../000_hello_acp/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/10_temporal/010_agent_chat -uv run agentex agents run --manifest manifest.yaml -``` - -## Key Pattern - -- **Streaming**: Progressive response generation with `adk.messages.create()` -- **Multi-turn**: Conversation history maintained in durable workflow state -- **Tools**: Agent can call functions to perform actions -- **Tracing**: Full observability of tool calls and LLM interactions -- **Durability**: All of the above survives worker restarts - -**Monitor:** Open Temporal UI at http://localhost:8233 to see the workflow and all tool call activities. - -## Key Insight - -In base agentic agents, all this state lives in memory and is lost on crash. With Temporal, the entire conversation - history, tool calls, intermediate state - is durably persisted. The agent can pick up a conversation that paused days ago as if no time passed. - -## When to Use -- Production chatbots with tool capabilities -- Long-running customer service conversations -- Agents that need both reliability and rich features -- Any conversational agent handling real user traffic - -## Why This Matters -This is the pattern for real production agents. By combining all capabilities (streaming, tools, tracing) with Temporal's durability, you get an agent that's both feature-rich and reliable. This is what enterprise conversational AI looks like. - -**Next:** [020_state_machine](../020_state_machine/) - Add complex multi-phase workflows +[010 Agent Chat Temporal Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/temporal/agent_chat/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/020_state_machine/README.md b/examples/tutorials/10_agentic/10_temporal/020_state_machine/README.md index 05e0fe1c..b1f25661 100644 --- a/examples/tutorials/10_agentic/10_temporal/020_state_machine/README.md +++ b/examples/tutorials/10_agentic/10_temporal/020_state_machine/README.md @@ -1,70 +1,7 @@ -# [Temporal] State Machine +# [Agentic] State Machine with Temporal -Build complex multi-state workflows using state machines with Temporal. This tutorial shows a "deep research" agent that transitions through states: clarify query → wait for input → perform research → wait for follow-ups. +This tutorial demonstrates how to use state machines to manage complex agentic workflows with Temporal in AgentEx agents. -## What You'll Learn -- Building state machines with Temporal sub-workflows -- Explicit state transitions and phase management -- When to use state machines vs simple workflows -- Handling complex multi-phase agent behaviors +## Official Documentation -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- Understanding of Temporal workflows (see [010_agent_chat](../010_agent_chat/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/10_temporal/020_state_machine -uv run agentex agents run --manifest manifest.yaml -``` - -**Monitor:** Open Temporal UI at http://localhost:8233 to see state transitions and sub-workflows. - -## Architecture - -The workflow uses three sub-workflows, each handling a specific state: -- `ClarifyUserQueryWorkflow` - Asks follow-up questions to understand user intent -- `WaitingForUserInputWorkflow` - Waits for user responses -- `PerformingDeepResearchWorkflow` - Executes the research with full context - -State transitions are explicit and tracked, with each sub-workflow handling its own logic. - -## Why State Machines Matter - -Complex agents often need to: -- Wait for user input at specific points -- Branch behavior based on conditions -- Orchestrate multiple steps with clear transitions -- Resume at the exact state after failures - -State machines provide this structure. Each state is a sub-workflow, and Temporal ensures transitions are durable and resumable. - -## Key Pattern - -```python -self.state_machine = DeepResearchStateMachine( - initial_state=DeepResearchState.WAITING_FOR_USER_INPUT, - states=[ - State(name=DeepResearchState.CLARIFYING, workflow=ClarifyWorkflow()), - State(name=DeepResearchState.RESEARCHING, workflow=ResearchWorkflow()), - ] -) - -await self.state_machine.transition(DeepResearchState.RESEARCHING) -``` - -This is an advanced pattern - only needed when your agent has complex, multi-phase behavior. - -## When to Use -- Multi-step processes with clear phases -- Workflows that wait for user input at specific points -- Operations with branching logic based on state -- Complex coordination patterns requiring explicit transitions - -## Why This Matters -State machines provide structure for complex agent behaviors. While simple agents can use basic workflows, complex agents benefit from explicit state management. Temporal ensures state transitions are durable and resumable, even after failures. - -**Next:** [030_custom_activities](../030_custom_activities/) - Extend workflows with custom activities +[020 State Machine Temporal Agentic](https://dev.agentex.scale.com/docs/tutorials/agentic/temporal/state_machine/) \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/030_custom_activities/README.md b/examples/tutorials/10_agentic/10_temporal/030_custom_activities/README.md index 92aa8a03..0be65ef7 100644 --- a/examples/tutorials/10_agentic/10_temporal/030_custom_activities/README.md +++ b/examples/tutorials/10_agentic/10_temporal/030_custom_activities/README.md @@ -1,106 +1,301 @@ -# [Temporal] Custom Activities +# at030-custom-activities - AgentEx Temporal Agent Template -Learn how to extend Temporal workflows with custom activities for external operations like API calls, database queries, or complex computations. +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. ## What You'll Learn -- How to define custom Temporal activities -- When to use activities vs inline workflow code -- Activity retry and timeout configuration -- Integrating external services into workflows -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- Understanding of basic Temporal workflows (see [000_hello_acp](../000_hello_acp/)) +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations -## Quick Start +## Running the Agent -**Terminal 1 - Start Worker:** +1. Run the agent locally: ```bash -cd examples/tutorials/10_agentic/10_temporal/030_custom_activities -uv run python project/run_worker.py +agentex agents run --manifest manifest.yaml ``` -**Terminal 2 - Run Agent:** -```bash -uv run agentex agents run --manifest manifest.yaml +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +030_custom_activities/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + ``` -**Terminal 3 - Test via Notebook:** +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: + ```bash +# Start Jupyter notebook (make sure you have jupyter installed) jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb ``` -## Key Concepts +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information + +The notebook automatically uses your agent name (`at030-custom-activities`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. -### Activities vs Workflow Code +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker -**Use activities for:** -- External API calls -- Database operations -- File I/O or network operations -- Non-deterministic operations (random, time, external state) +### 4. Manage Dependencies -**Use workflow code for:** -- Orchestration logic -- State management -- Decision making based on activity results -### Defining a Custom Activity +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv +uv run agentex agents run --manifest manifest.yaml +``` + +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards + + + +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: ```python -# In project/activities.py -from temporalio import activity +import os +from dotenv import load_dotenv -@activity.defn -async def call_external_api(endpoint: str, data: dict) -> dict: - """Activities can perform non-deterministic operations.""" - import httpx - async with httpx.AsyncClient() as client: - response = await client.post(endpoint, json=data) - return response.json() +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development + +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` + +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n 030_custom_activities python=3.12 +conda activate 030_custom_activities +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** + +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent at030-custom-activities --task "Your task here" +``` + +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml ``` -### Using Activities in Workflows +## Advanced Features + +### Temporal Workflows +Extend your agent with sophisticated async workflows: ```python # In project/workflow.py -from temporalio import workflow - @workflow.defn class MyWorkflow(BaseWorkflow): - @workflow.run - async def run(self, input: dict): - # Activities are executed with retry and timeout policies - result = await workflow.execute_activity( - call_external_api, - args=["https://api.example.com", input], - start_to_close_timeout=timedelta(seconds=30), - retry_policy=RetryPolicy(maximum_attempts=3) - ) - return result + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass +``` + +### Custom Activities +Add custom activities for external operations: + +```python +# In project/activities.py +@activity.defn +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass +``` + +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis ``` -## Try It -1. Modify `project/activities.py` to add a new activity -2. Update `project/workflow.py` to call your activity -3. Register the activity in `project/run_worker.py` -4. Restart the worker and test via the notebook -5. Check Temporal UI at http://localhost:8233 to see activity execution and retries +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed + -## When to Use -- Integrating external services (OpenAI, databases, APIs) -- Operations that may fail and need automatic retries -- Long-running computations that should be checkpointed -- Separating business logic from orchestration +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes -## Why This Matters -Activities are Temporal's way of handling the real world's messiness: network failures, API rate limits, and transient errors. They provide automatic retries, timeouts, and observability for operations that would otherwise require extensive error handling code. +### Temporal-Specific Troubleshooting ---- +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker -**For detailed setup instructions, see [TEMPLATE_GUIDE.md](./TEMPLATE_GUIDE.md)** +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues -**Next:** [050_agent_chat_guardrails](../050_agent_chat_guardrails/) - Add safety and validation to your workflows +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails/README.md b/examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails/README.md index 8f6d3bc1..9eb75f98 100644 --- a/examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails/README.md +++ b/examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails/README.md @@ -1,27 +1,10 @@ -# [Temporal] Agent Chat with Guardrails +# [Agentic] Agent Chat with Guardrails This tutorial demonstrates how to implement streaming multiturn tool-enabled chat with input and output guardrails using Temporal workflows in AgentEx agents. -## What You'll Learn -- Adding safety guardrails to conversational agents -- Input validation and output filtering -- Implementing content moderation with Temporal -- When to block vs warn vs allow content +## Overview -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- Understanding of agent chat patterns (see [010_agent_chat](../010_agent_chat/)) - -## Quick Start - -```bash -cd examples/tutorials/10_agentic/10_temporal/050_agent_chat_guardrails -uv run agentex agents run --manifest manifest.yaml -``` - -**Monitor:** Open Temporal UI at http://localhost:8233 to see guardrail checks as workflow activities. +This example extends the basic agent chat functionality by adding guardrails that can filter both user inputs and AI outputs. This is useful for content moderation, compliance, or preventing certain topics from being discussed. ## Guardrails @@ -56,15 +39,4 @@ The guardrails are implemented as functions that: - `output_info`: Metadata about the check - `rejection_message`: Custom message shown when content is blocked -See `workflow.py` for the complete implementation. - -## When to Use -- Content moderation and safety requirements -- Compliance with regulatory restrictions -- Brand safety and reputation protection -- Preventing agents from discussing sensitive topics - -## Why This Matters -Production agents need safety rails. This pattern shows how to implement content filtering without sacrificing the benefits of Temporal workflows. Guardrail checks become durable activities, visible in Temporal UI for audit and debugging. - -**Next:** [060_open_ai_agents_sdk_hello_world](../060_open_ai_agents_sdk_hello_world/) - Integrate OpenAI Agents SDK with Temporal \ No newline at end of file +See `workflow.py` for the complete implementation. \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md index 6594f105..c46722b8 100644 --- a/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md +++ b/examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/README.md @@ -1,105 +1,317 @@ -# [Temporal] OpenAI Agents SDK - Hello World +# example-tutorial - AgentEx Temporal Agent Template -**Part of the [OpenAI SDK + Temporal integration series](../README.md)** +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. ## What You'll Learn -The OpenAI Agents SDK plugin automatically converts LLM calls into durable Temporal activities. When `Runner.run()` executes, the LLM invocation becomes an `invoke_model_activity` visible in Temporal UI with full observability, automatic retries, and durability. +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations -**Key insight:** You don't need to wrap agent calls in activities manually - the plugin handles this automatically, making non-deterministic LLM calls work seamlessly in Temporal workflows. +## Running the Agent -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root (includes Temporal) -- Temporal UI available at http://localhost:8233 -- OpenAI API key configured (see setup below) -- Understanding of Temporal workflows (see [000_hello_acp](../000_hello_acp/)) +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` -## Setup +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. -This tutorial uses the OpenAI Agents SDK plugin, which needs to be added in two places: +## What's Inside -### 1. Add Plugin to ACP (`project/acp.py`) -```python -from agentex.lib.plugins.openai_agents import OpenAIAgentsPlugin +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure -acp = FastACP.create( - config=TemporalACPConfig( - plugins=[OpenAIAgentsPlugin()] # Add this - ) -) ``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing -### 2. Add Plugin to Worker (`project/run_worker.py`) -```python -from agentex.lib.plugins.openai_agents import OpenAIAgentsPlugin +└── pyproject.toml # Dependencies (uv) -worker = AgentexWorker( - task_queue=task_queue_name, - plugins=[OpenAIAgentsPlugin()], # Add this -) ``` -### 3. Configure OpenAI API Key -Add to `manifest.yaml`: -```yaml -secrets: - - name: OPENAI_API_KEY - value: "your-openai-api-key-here" +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: + +```bash +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb ``` -Or set in `.env` file: `OPENAI_API_KEY=your-key-here` +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information -## Quick Start +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. + +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker + +### 4. Manage Dependencies + + +You chose **uv** for package management. Here's how to work with dependencies: ```bash -cd examples/tutorials/10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv uv run agentex agents run --manifest manifest.yaml ``` -**Monitor:** Open Temporal UI at http://localhost:8233 to see automatic activity creation. +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards + + + +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: + +```python +import os +from dotenv import load_dotenv + +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development + +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` + +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** -## Try It +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" +``` -1. Send a message to the agent (it responds in haikus) -2. Check the agent response: +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev -![Agent Response](../_images/hello_world_response.png) +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` -3. Open Temporal UI at http://localhost:8233 -4. Find your workflow execution -5. Look for the `invoke_model_activity` - this was created automatically: +## Advanced Features -![Temporal UI](../_images/hello_world_temporal.png) +### Temporal Workflows +Extend your agent with sophisticated async workflows: -6. Inspect the activity to see: - - Input parameters (your message) - - Output (agent's haiku response) - - Execution time - - Retry attempts (if any failures occurred) +```python +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass +``` -## Key Code +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): ```python -# This simple call automatically becomes a durable Temporal activity: -agent = Agent(name="Haiku Assistant", instructions="...") -result = await Runner.run(agent, user_message) +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy +) + +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] ``` -The magic happens behind the scenes - no manual activity wrapping needed. The conversation is now durable and survives process restarts. +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis +``` + + +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed -## Why This Matters -**Durability:** If your worker crashes mid-conversation, Temporal resumes exactly where it left off. No lost context, no repeated work. +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes -**Observability:** Every LLM call is tracked as an activity with full execution history. +### Temporal-Specific Troubleshooting -**Reliability:** Failed LLM calls are automatically retried with exponential backoff. +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker -## When to Use -- Building agents with OpenAI's SDK -- Need durability for LLM calls -- Want automatic activity creation without manual wrapping -- Leveraging OpenAI's agent patterns with Temporal's durability +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues -**Next:** [070_open_ai_agents_sdk_tools](../070_open_ai_agents_sdk_tools/) - Add durable tools to your agents +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md index 9dee3544..c46722b8 100644 --- a/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md +++ b/examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools/README.md @@ -1,180 +1,317 @@ -# [Temporal] OpenAI Agents SDK - Tools +# example-tutorial - AgentEx Temporal Agent Template -**Part of the [OpenAI SDK + Temporal integration series](../README.md)** → Previous: [060 Hello World](../060_open_ai_agents_sdk_hello_world/) +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. ## What You'll Learn -Two patterns for making agent tools durable with Temporal: +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations -**Pattern 1: `activity_as_tool()`** - Single activity per tool call -- Use for: Single API calls, DB queries, external operations -- Example: `get_weather` tool → creates one `get_weather` activity -- 1:1 mapping between tool calls and activities +## Running the Agent -**Pattern 2: Function tools with multiple activities** - Multiple activities per tool call -- Use for: Multi-step operations needing guaranteed sequencing -- Example: `move_money` tool → creates `withdraw_money` activity THEN `deposit_money` activity -- 1:many mapping - your code controls execution order, not the LLM -- Ensures atomic operations (withdraw always happens before deposit) +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- OpenAI Agents SDK plugin configured (see [060_hello_world](../060_open_ai_agents_sdk_hello_world/)) +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + +``` -## Quick Start +## Development + +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management + +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: ```bash -cd examples/tutorials/10_agentic/10_temporal/070_open_ai_agents_sdk_tools +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb +``` + +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information + +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. + +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker + +### 4. Manage Dependencies + + +You chose **uv** for package management. Here's how to work with dependencies: + +```bash +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv uv run agentex agents run --manifest manifest.yaml ``` -**Monitor:** Open Temporal UI at http://localhost:8233 to see tool calls as activities. +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards -## Try It -### Pattern 1: Single Activity Tool -Ask "What's the weather in San Francisco?" +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: -1. Check the agent response: +```python +import os +from dotenv import load_dotenv + +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` + +## Local Development -![Weather Response](../_images/weather_response.png) +### 1. Start the Agentex Backend +```bash +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd +``` -2. Open Temporal UI (localhost:8233) -3. See a single `get_weather` activity created: +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate + +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** + +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" +``` -![Weather Activity](../_images/weather_activity_tool.png) +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev -The activity shows the external call with retry capability. Each step (model invocation → tool call → model invocation) is durable. +# Then open http://localhost:3000 in your browser to chat with your agent +``` -### Pattern 2: Multi-Activity Tool (Optional) +## Development Tips -To try the advanced banking example, uncomment the `move_money` sections in the code, then ask to move money. +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode -1. Check the agent response: +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services -![Money Transfer Response](../_images/move_money_response.png) +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations -2. Open Temporal UI and see TWO sequential activities: +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging -![Money Transfer Workflow](../_images/move_money_temporal.png) +### To build the agent Docker image locally (normally not necessary): -- First: `withdraw_money` activity executes -- Then: `deposit_money` activity executes -- Each activity shows its parameters and execution time +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` -**Critical insight:** If the system crashes after withdraw but before deposit, Temporal resumes exactly where it left off. The deposit will still happen - guaranteed transactional integrity. +## Advanced Features -## Key Code +### Temporal Workflows +Extend your agent with sophisticated async workflows: -### Pattern 1: Single Activity Tool ```python -# Define the activity -@activity.defn -async def get_weather(city: str) -> str: - """Get the weather for a given city""" - # This could be an API call - Temporal handles retries - return f"The weather in {city} is sunny" - -# Use activity_as_tool to convert it -weather_agent = Agent( - name="Weather Assistant", - instructions="Use the get_weather tool to answer weather questions.", - tools=[ - activity_as_tool(get_weather, start_to_close_timeout=timedelta(seconds=10)) - ] -) +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass ``` -### Pattern 2: Multi-Activity Tool +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): + ```python -# Define individual activities -@activity.defn -async def withdraw_money(from_account: str, amount: float) -> str: - # Simulate API call - await asyncio.sleep(5) - return f"Withdrew ${amount} from {from_account}" - -@activity.defn -async def deposit_money(to_account: str, amount: float) -> str: - # Simulate API call - await asyncio.sleep(10) - return f"Deposited ${amount} into {to_account}" - -# Create a function tool that orchestrates both activities -@function_tool -async def move_money(from_account: str, to_account: str, amount: float) -> str: - """Move money from one account to another""" - - # Step 1: Withdraw (becomes an activity) - await workflow.start_activity( - "withdraw_money", - args=[from_account, amount], - start_to_close_timeout=timedelta(days=1) - ) - - # Step 2: Deposit (becomes an activity) - await workflow.start_activity( - "deposit_money", - args=[to_account, amount], - start_to_close_timeout=timedelta(days=1) - ) - - return "Money transferred successfully" - -# Use the tool in your agent -money_agent = Agent( - name="Money Mover", - instructions="Use move_money to transfer funds between accounts.", - tools=[move_money] +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy ) + +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] ``` -## When to Use Each Pattern +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis +``` + + +## Troubleshooting -### Use Pattern 1 when: -- Tool performs a single external operation (API call, DB query) -- Operation is already idempotent -- No sequencing guarantees needed +### Common Issues -### Use Pattern 2 when: -- Tool requires multiple sequential operations -- Order must be guaranteed (withdraw THEN deposit) -- Operations need to be atomic from the agent's perspective -- You want transactional integrity across steps +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors -## Why This Matters +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors -**Without Temporal:** If you withdraw money but crash before depositing, you're stuck in a broken state. The money is gone from the source account with no way to recover. +3. **Dependency issues** -**With Temporal (Pattern 2):** -- Guaranteed execution with exact resumption after failures -- If the system crashes after withdraw, Temporal resumes and completes deposit -- Each step is tracked and retried independently -- Full observability of the entire operation + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed -**Key insight:** Pattern 2 moves sequencing control from the LLM (which might call tools in wrong order) to your deterministic code (which guarantees correct order). The LLM still decides *when* to call the tool, but your code controls *how* the operations execute. -This makes agents production-ready for: -- Financial transactions -- Order fulfillment workflows -- Multi-step API integrations -- Any operation where partial completion is dangerous +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes -## When to Use +### Temporal-Specific Troubleshooting -**Pattern 1 (activity_as_tool):** -- Single API calls -- Database queries -- External service integrations -- Operations that are naturally atomic +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker -**Pattern 2 (Multi-activity tools):** -- Financial transactions requiring sequencing -- Multi-step operations with dependencies -- Operations where order matters critically -- Workflows needing guaranteed atomicity +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues -**Next:** [080_open_ai_agents_sdk_human_in_the_loop](../080_open_ai_agents_sdk_human_in_the_loop/) - Add human approval workflows +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md index 2c7ea850..c46722b8 100644 --- a/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md +++ b/examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/README.md @@ -1,199 +1,317 @@ -# [Temporal] OpenAI Agents SDK - Human in the Loop +# example-tutorial - AgentEx Temporal Agent Template -**Part of the [OpenAI SDK + Temporal integration series](../README.md)** → Previous: [070 Tools](../070_open_ai_agents_sdk_tools/) +This is a starter template for building asynchronous agents with the AgentEx framework and Temporal. It provides a basic implementation of the Agent 2 Client Protocol (ACP) with Temporal workflow support to help you get started quickly. ## What You'll Learn -How to pause agent execution and wait indefinitely for human approval using Temporal's child workflows and signals. The agent can wait for hours, days, or weeks for human input without consuming resources - and if the system crashes, it resumes exactly where it left off. +- **Tasks**: A task is a grouping mechanism for related messages. Think of it as a conversation thread or a session. +- **Messages**: Messages are communication objects within a task. They can contain text, data, or instructions. +- **ACP Events**: The agent responds to four main events: + - `task_received`: When a new task is created + - `task_message_received`: When a message is sent within a task + - `task_approved`: When a task is approved + - `task_canceled`: When a task is canceled +- **Temporal Workflows**: Long-running processes that can handle complex state management and async operations -**Pattern:** -1. Agent calls `wait_for_confirmation` tool -2. Tool spawns a child workflow that waits for a signal -3. Human approves/rejects via Temporal CLI or web UI -4. Child workflow completes, agent continues with the response +## Running the Agent -## New Temporal Concepts +1. Run the agent locally: +```bash +agentex agents run --manifest manifest.yaml +``` + +The agent will start on port 8000 and print messages whenever it receives any of the ACP events. + +## What's Inside + +This template: +- Sets up a basic ACP server with Temporal integration +- Handles each of the required ACP events +- Provides a foundation for building complex async agents +- Includes Temporal workflow and activity definitions + +## Next Steps + +For more advanced agent development, check out the AgentEx tutorials: + +- **Tutorials 00-08**: Learn about building synchronous agents with ACP +- **Tutorials 09-10**: Learn how to use Temporal to power asynchronous agents + - Tutorial 09: Basic Temporal workflow setup + - Tutorial 10: Advanced Temporal patterns and best practices + +These tutorials will help you understand: +- How to handle long-running tasks +- Implementing state machines +- Managing complex workflows +- Best practices for async agent development + +## The Manifest File + +The `manifest.yaml` file is your agent's configuration file. It defines: +- How your agent should be built and packaged +- What files are included in your agent's Docker image +- Your agent's name and description +- Local development settings (like the port your agent runs on) +- Temporal worker configuration + +This file is essential for both local development and deployment of your agent. + +## Project Structure + +``` +example_tutorial/ +├── project/ # Your agent's code +│ ├── __init__.py +│ ├── acp.py # ACP server and event handlers +│ ├── workflow.py # Temporal workflow definitions +│ ├── activities.py # Temporal activity definitions +│ └── run_worker.py # Temporal worker setup +├── Dockerfile # Container definition +├── manifest.yaml # Deployment config +├── dev.ipynb # Development notebook for testing + +└── pyproject.toml # Dependencies (uv) + +``` -### Signals -Signals are a way for external systems to interact with running workflows. Think of them as secure, durable messages sent to your workflow from the outside world. +## Development -**Use cases:** -- User approving/rejecting an action in a web app -- Payment confirmation triggering shipping -- Live data feeds (stock prices) triggering trades -- Webhooks from external services updating workflow state +### 1. Customize Event Handlers +- Modify the handlers in `acp.py` to implement your agent's logic +- Add your own tools and capabilities +- Implement custom state management -**How it works:** Define a function in your workflow class with the `@workflow.signal` decorator. External systems can then send signals using: -- Temporal SDK (by workflow ID) -- Another Temporal workflow -- Temporal CLI -- Temporal Web UI +### 2. Test Your Agent with the Development Notebook +Use the included `dev.ipynb` Jupyter notebook to test your agent interactively: -[Learn more about signals](https://docs.temporal.io/develop/python/message-passing#send-signal-from-client) +```bash +# Start Jupyter notebook (make sure you have jupyter installed) +jupyter notebook dev.ipynb + +# Or use VS Code to open the notebook directly +code dev.ipynb +``` -### Child Workflows -Child workflows are like spawning a new workflow from within your current workflow. Similar to calling a function in traditional programming, but the child workflow: -- Runs independently with its own execution history -- Inherits all Temporal durability guarantees -- Can be monitored separately in Temporal UI -- Continues running even if the parent has issues +The notebook includes: +- **Setup**: Connect to your local AgentEx backend +- **Task creation**: Create a new task for the conversation +- **Event sending**: Send events to the agent and get responses +- **Async message subscription**: Subscribe to server-side events to receive agent responses +- **Rich message display**: Beautiful formatting with timestamps and author information -**Why use child workflows for human-in-the-loop?** -- The parent workflow can continue processing while waiting -- The child workflow can wait indefinitely for human input -- Full isolation between waiting logic and main agent logic -- Clean separation of concerns +The notebook automatically uses your agent name (`example-tutorial`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses. -[Learn more about child workflows](https://docs.temporal.io/develop/python/child-workflows) +### 3. Develop Temporal Workflows +- Edit `workflow.py` to define your agent's async workflow logic +- Modify `activities.py` to add custom activities +- Use `run_worker.py` to configure the Temporal worker -## Prerequisites -- Development environment set up (see [main repo README](https://github.com/scaleapi/scale-agentex)) -- Backend services running: `make dev` from repository root -- Temporal UI available at http://localhost:8233 -- OpenAI Agents SDK plugin configured (see [060_hello_world](../060_open_ai_agents_sdk_hello_world/)) -- Understanding of tools (see [070_tools](../070_open_ai_agents_sdk_tools/)) +### 4. Manage Dependencies -## Quick Start + +You chose **uv** for package management. Here's how to work with dependencies: ```bash -cd examples/tutorials/10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop +# Add new dependencies +agentex uv add requests openai anthropic + +# Add Temporal-specific dependencies (already included) +agentex uv add temporalio + +# Install/sync dependencies +agentex uv sync + +# Run commands with uv uv run agentex agents run --manifest manifest.yaml ``` -**Monitor:** Open Temporal UI at http://localhost:8233 to see child workflows and signals. +**Benefits of uv:** +- Faster dependency resolution and installation +- Better dependency isolation +- Modern Python packaging standards -## Try It -1. Ask the agent to do something that requires approval (e.g., "Order 100 widgets") -2. The agent will call `wait_for_confirmation` and pause -3. Open Temporal UI (localhost:8233) -4. Find the parent workflow - you'll see it's waiting on the child workflow: -![Parent Workflow Waiting](../_images/human_in_the_loop_workflow.png) +### 5. Configure Credentials +- Add any required credentials to your manifest.yaml +- For local development, create a `.env` file in the project directory +- Use `load_dotenv()` only in development mode: -5. Find the child workflow - it's waiting for a signal: +```python +import os +from dotenv import load_dotenv -![Child Workflow Waiting](../_images/human_in_the_loop_child_workflow.png) +if os.environ.get("ENVIRONMENT") == "development": + load_dotenv() +``` -6. Send approval signal via CLI: +## Local Development +### 1. Start the Agentex Backend ```bash -temporal workflow signal \ - --workflow-id="" \ - --name="fulfill_order_signal" \ - --input=true +# Navigate to the backend directory +cd agentex + +# Start all services using Docker Compose +make dev + +# Optional: In a separate terminal, use lazydocker for a better UI (everything should say "healthy") +lzd ``` -7. Watch both workflows complete - the agent resumes and finishes the action +### 2. Setup Your Agent's requirements/pyproject.toml +```bash +agentex uv sync [--group editable-apy] +source .venv/bin/activate -## Key Code +# OR +conda create -n example_tutorial python=3.12 +conda activate example_tutorial +pip install -r requirements.txt +``` +### 3. Run Your Agent +```bash +# From this directory +export ENVIRONMENT=development && [uv run] agentex agents run --manifest manifest.yaml +``` +4. **Interact with your agent** -### The Tool: Spawning a Child Workflow -```python -from agents import function_tool -from temporalio import workflow -from project.child_workflow import ChildWorkflow -from temporalio.workflow import ParentClosePolicy - -@function_tool -async def wait_for_confirmation(confirmation: bool) -> str: - """Wait for human confirmation before proceeding""" - - # Spawn a child workflow that will wait for a signal - result = await workflow.execute_child_workflow( - ChildWorkflow.on_task_create, - environment_variables.WORKFLOW_NAME + "_child", - id="child-workflow-id", - parent_close_policy=ParentClosePolicy.TERMINATE, - ) - - return result +Option 0: CLI (deprecated - to be replaced once a new CLI is implemented - please use the web UI for now!) +```bash +# Submit a task via CLI +agentex tasks submit --agent example-tutorial --task "Your task here" ``` -### The Child Workflow: Waiting for Signals +Option 1: Web UI +```bash +# Start the local web interface +cd agentex-web +make dev + +# Then open http://localhost:3000 in your browser to chat with your agent +``` + +## Development Tips + +### Environment Variables +- Set environment variables in project/.env for any required credentials +- Or configure them in the manifest.yaml under the `env` section +- The `.env` file is automatically loaded in development mode + +### Local Testing +- Use `export ENVIRONMENT=development` before running your agent +- This enables local service discovery and debugging features +- Your agent will automatically connect to locally running services + +### Temporal-Specific Tips +- Monitor workflows in the Temporal Web UI at http://localhost:8080 +- Use the Temporal CLI for advanced workflow management +- Check workflow logs for debugging async operations + +### Debugging +- Check agent logs in the terminal where you ran the agent +- Use the web UI to inspect task history and responses +- Monitor backend services with `lzd` (LazyDocker) +- Use Temporal Web UI for workflow debugging + +### To build the agent Docker image locally (normally not necessary): + +1. Build the agent image: +```bash +agentex agents build --manifest manifest.yaml +``` + +## Advanced Features + +### Temporal Workflows +Extend your agent with sophisticated async workflows: + ```python -import asyncio -from temporalio import workflow - -@workflow.defn(name=environment_variables.WORKFLOW_NAME + "_child") -class ChildWorkflow(): - def __init__(self): - # Queue to hold signals - self._pending_confirmation: asyncio.Queue[bool] = asyncio.Queue() - - @workflow.run - async def on_task_create(self, name: str) -> str: - logger.info(f"Child workflow started: {name}") - - # Wait indefinitely until we receive a signal - await workflow.wait_condition( - lambda: not self._pending_confirmation.empty() - ) - - # Signal received - complete the workflow - return "Task completed" - - @workflow.signal - async def fulfill_order_signal(self, success: bool) -> None: - """External systems call this to approve/reject""" - if success: - await self._pending_confirmation.put(True) +# In project/workflow.py +@workflow.defn +class MyWorkflow(BaseWorkflow): + async def complex_operation(self): + # Multi-step async operations + # Error handling and retries + # State management + pass ``` -### Using the Tool in Your Agent +### Custom Activities +Add custom activities for external operations. **Important**: Always specify appropriate timeouts (recommended: 10 minutes): + ```python -confirm_order_agent = Agent( - name="Confirm Order", - instructions="When user asks to confirm an order, use wait_for_confirmation tool.", - tools=[wait_for_confirmation] +# In project/activities.py +from datetime import timedelta +from temporalio import activity +from temporalio.common import RetryPolicy + +@activity.defn(name="call_external_api") +async def call_external_api(data): + # HTTP requests, database operations, etc. + pass + +# In your workflow, call it with a timeout: +result = await workflow.execute_activity( + "call_external_api", + data, + start_to_close_timeout=timedelta(minutes=10), # Recommended: 10 minute timeout + heartbeat_timeout=timedelta(minutes=1), # Optional: heartbeat monitoring + retry_policy=RetryPolicy(maximum_attempts=3) # Optional: retry policy ) -result = await Runner.run(confirm_order_agent, params.event.content.content) +# Don't forget to register your custom activities in run_worker.py: +# all_activities = get_all_activities() + [your_custom_activity_function] +``` + +### Integration with External Services + +```bash +# Add service clients +agentex uv add httpx requests-oauthlib + +# Add AI/ML libraries +agentex uv add openai anthropic transformers + +# Add database clients +agentex uv add asyncpg redis ``` -## How It Works - -1. **Agent calls tool**: The LLM decides to call `wait_for_confirmation` -2. **Child workflow spawned**: A new workflow is created with its own ID -3. **Child waits**: Uses `workflow.wait_condition()` to block until signal arrives -4. **Parent waits**: Parent workflow is blocked waiting for child to complete -5. **Signal sent**: External system (CLI, web app, API) sends signal with workflow ID -6. **Signal received**: Child workflow's `fulfill_order_signal()` method is called -7. **Queue updated**: Signal handler adds item to queue -8. **Wait condition satisfied**: `wait_condition()` unblocks -9. **Child completes**: Returns result to parent -10. **Parent resumes**: Agent continues with the response - -**Critical insight:** At any point, if the system crashes: -- Both workflows are durable and will resume -- No context is lost -- The moment the signal arrives, execution continues - -## Why This Matters - -**Without Temporal:** If your system crashes while waiting for human approval, you lose all context about what was being approved. The user has to start over. - -**With Temporal:** -- The workflow waits durably (hours, days, weeks) -- If the system crashes and restarts, context is preserved -- The moment a human sends approval, workflow resumes exactly where it left off -- Full audit trail of who approved what and when - -**Production use cases:** -- **Financial transactions**: Agent initiates transfer, human approves -- **Legal document processing**: AI extracts data, lawyer reviews -- **Multi-step purchasing**: Agent negotiates, manager approves -- **Compliance workflows**: System flags issue, human decides action -- **High-stakes decisions**: Any operation requiring human judgment - -This pattern transforms agents from fully automated systems into **collaborative AI assistants** that know when to ask for help. - -## When to Use -- Financial transactions requiring approval -- High-stakes decisions needing human judgment -- Compliance workflows with mandatory review steps -- Legal or contractual operations -- Any operation where errors have serious consequences -- Workflows where AI assists but humans decide - -**Congratulations!** You've completed all AgentEx tutorials. You now know how to build production-ready agents from simple sync patterns to complex durable workflows with human oversight. + +## Troubleshooting + +### Common Issues + +1. **Agent not appearing in web UI** + - Check if agent is running on port 8000 + - Verify `ENVIRONMENT=development` is set + - Check agent logs for errors + +2. **Temporal workflow issues** + - Check Temporal Web UI at http://localhost:8080 + - Verify Temporal server is running in backend services + - Check workflow logs for specific errors + +3. **Dependency issues** + + - Run `agentex uv sync` to ensure all dependencies are installed + - Verify temporalio is properly installed + + +4. **Port conflicts** + - Check if another service is using port 8000 + - Use `lsof -i :8000` to find conflicting processes + +### Temporal-Specific Troubleshooting + +1. **Workflow not starting** + - Check if Temporal server is running (`docker ps`) + - Verify task queue configuration in `run_worker.py` + - Check workflow registration in the worker + +2. **Activity failures** + - Check activity logs in the console + - Verify activity registration + - Check for timeout issues + +Happy building with Temporal! 🚀⚡ \ No newline at end of file diff --git a/examples/tutorials/README.md b/examples/tutorials/README.md deleted file mode 100644 index ecdd2225..00000000 --- a/examples/tutorials/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# AgentEx Tutorials - -Progressive tutorials for learning AgentEx from basics to production-ready patterns. - -## Prerequisites - -**Before starting any tutorial:** -1. Set up your development environment following the [main repo README](https://github.com/scaleapi/scale-agentex#setup) -2. Start backend services from repository root: - ```bash - cd /path/to/agentex-python - make dev - ``` -3. Verify Temporal UI is accessible at http://localhost:8233 - -For troubleshooting, see the [AgentEx debugging guide](https://github.com/scaleapi/scale-agentex#troubleshooting). - -## Learning Path - -```mermaid -graph TD - A[👋 Start Here] --> B[00_sync/000_hello_acp] - B --> C[00_sync/010_multiturn] - C --> D[00_sync/020_streaming] - - D --> E{Need Task
Management?} - E -->|Yes| F[10_agentic/00_base/
000_hello_acp] - E -->|No| G[Continue with
sync patterns] - - F --> H[00_base/010_multiturn] - H --> I[00_base/020_streaming] - I --> J[00_base/030_tracing] - J --> K[00_base/040_other_sdks] - K --> L[00_base/080_batch_events] - - L --> M{Building for
Production?} - M -->|Yes| N[10_temporal/
000_hello_acp] - M -->|No| O[00_base/090_multi_agent] - - N --> P[10_temporal/010_agent_chat] - P --> Q[10_temporal/020_state_machine] - Q --> R[10_temporal/030_custom_activities] - R --> S[10_temporal/050_guardrails] - - S --> T{Using
OpenAI SDK?} - T -->|Yes| U[10_temporal/060_openai_hello] - U --> V[10_temporal/070_openai_tools] - V --> W[10_temporal/080_openai_hitl] - T -->|No| X[🎉 Production Ready!] - W --> X - - style A fill:#e1f5e1 - style X fill:#fff3cd - style E fill:#e3f2fd - style M fill:#e3f2fd - style T fill:#e3f2fd -``` - -## Tutorial Structure - -### 00_sync/ - Synchronous Agents -Simple request-response patterns without task management. Start here if you're new to AgentEx. - -- **[000_hello_acp](00_sync/000_hello_acp/)** - Your first agent -- **[010_multiturn](00_sync/010_multiturn/)** - Maintaining conversation context -- **[020_streaming](00_sync/020_streaming/)** - Real-time response streaming - -**When to use:** Simple chatbots, stateless Q&A, quick prototypes - ---- - -### 10_agentic/ - Task-Based Agents - -#### 00_base/ - Non-Temporal Patterns -Task-based architecture without workflow orchestration. Adds task management on top of sync patterns. - -- **[000_hello_acp](10_agentic/00_base/000_hello_acp/)** - Task-based hello world -- **[010_multiturn](10_agentic/00_base/010_multiturn/)** - Multiturn with task management -- **[020_streaming](10_agentic/00_base/020_streaming/)** - Streaming with tasks -- **[030_tracing](10_agentic/00_base/030_tracing/)** - Observability with Scale Groundplane -- **[040_other_sdks](10_agentic/00_base/040_other_sdks/)** - Integrating OpenAI, Anthropic, etc. -- **[080_batch_events](10_agentic/00_base/080_batch_events/)** - Event batching (shows limitations → Temporal) -- **[090_multi_agent_non_temporal](10_agentic/00_base/090_multi_agent_non_temporal/)** - Complex multi-agent coordination - -**When to use:** Task tracking needed but workflows are simple, no durability requirements - ---- - -#### 10_temporal/ - Production Workflows -Durable, fault-tolerant agents with Temporal workflow orchestration. - -**Core Patterns:** -- **[000_hello_acp](10_agentic/10_temporal/000_hello_acp/)** - Temporal basics -- **[010_agent_chat](10_agentic/10_temporal/010_agent_chat/)** - Stateful conversations -- **[020_state_machine](10_agentic/10_temporal/020_state_machine/)** - Structured state management -- **[030_custom_activities](10_agentic/10_temporal/030_custom_activities/)** - Custom Temporal activities -- **[050_agent_chat_guardrails](10_agentic/10_temporal/050_agent_chat_guardrails/)** - Safety & validation - -**OpenAI Agents SDK Series:** -- **[060_openai_hello_world](10_agentic/10_temporal/060_open_ai_agents_sdk_hello_world/)** - Plugin-based agents -- **[070_openai_tools](10_agentic/10_temporal/070_open_ai_agents_sdk_tools/)** - Tool integration patterns -- **[080_openai_hitl](10_agentic/10_temporal/080_open_ai_agents_sdk_human_in_the_loop/)** - Human oversight workflows - -**When to use:** Production systems requiring durability, fault tolerance, long-running workflows, or complex state management - ---- - -## Quick Start - -```bash -# 1. Start backend services (from repo root) -make dev - -# 2. Navigate to a tutorial -cd examples/tutorials/00_sync/000_hello_acp - -# 3. Run it -uv run python hello_acp.py -``` - -## Common Commands - -```bash -# Format tutorial code (always scope to specific files you're modifying) -rye run format examples/tutorials/00_sync/000_hello_acp/ - -# Run all agentic tutorial tests -cd examples/tutorials -./run_all_agentic_tests.sh - -# Run specific tutorial test -cd examples/tutorials -uv run pytest 00_sync/000_hello_acp/ -v - -# Check Temporal UI (when running temporal tutorials) -open http://localhost:8233 -``` - -## Tutorial Categories at a Glance - -| Category | Tutorials | Focus | Use When | -|----------|-----------|-------|----------| -| **Sync** | 3 | Request-response basics | Learning fundamentals, simple chatbots | -| **Agentic Base** | 7 | Task management without workflows | Need task tracking, simple coordination | -| **Temporal** | 8 | Production-grade workflows | Need durability, fault tolerance, complex state | - -## Getting Help - -- **Each tutorial includes:** README explaining concepts, annotated source code, and tests -- **Common issues?** See [AgentEx troubleshooting guide](https://github.com/scaleapi/scale-agentex#troubleshooting) -- **Need more context?** Check the [main AgentEx documentation](https://github.com/scaleapi/scale-agentex) - ---- - -**Ready to start?** → Begin with [00_sync/000_hello_acp](00_sync/000_hello_acp/) diff --git a/pyproject.toml b/pyproject.toml index 1e7bf72d..cc1a2607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agentex-sdk" -version = "0.5.1" +version = "0.5.2" description = "The official Python library for the agentex API" dynamic = ["readme"] license = "Apache-2.0" @@ -62,8 +62,8 @@ classifiers = [ ] [project.urls] -Homepage = "https://github.com/scaleapi/agentex-python" -Repository = "https://github.com/scaleapi/agentex-python" +Homepage = "https://github.com/scaleapi/scale-agentex-python" +Repository = "https://github.com/scaleapi/scale-agentex-python" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] @@ -165,7 +165,7 @@ path = "README.md" [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] # replace relative links with absolute links pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' -replacement = '[\1](https://github.com/scaleapi/agentex-python/tree/main/\g<2>)' +replacement = '[\1](https://github.com/scaleapi/scale-agentex-python/tree/main/\g<2>)' [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/agentex/_version.py b/src/agentex/_version.py index 67706282..4729ac14 100644 --- a/src/agentex/_version.py +++ b/src/agentex/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "agentex" -__version__ = "0.5.1" # x-release-please-version +__version__ = "0.5.2" # x-release-please-version diff --git a/src/agentex/resources/agents.py b/src/agentex/resources/agents.py index 9f4cffee..a479781c 100644 --- a/src/agentex/resources/agents.py +++ b/src/agentex/resources/agents.py @@ -43,7 +43,7 @@ def with_raw_response(self) -> AgentsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AgentsResourceWithRawResponse(self) @@ -52,7 +52,7 @@ def with_streaming_response(self) -> AgentsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AgentsResourceWithStreamingResponse(self) @@ -603,7 +603,7 @@ def with_raw_response(self) -> AsyncAgentsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncAgentsResourceWithRawResponse(self) @@ -612,7 +612,7 @@ def with_streaming_response(self) -> AsyncAgentsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncAgentsResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/events.py b/src/agentex/resources/events.py index f6740590..64ac6326 100644 --- a/src/agentex/resources/events.py +++ b/src/agentex/resources/events.py @@ -31,7 +31,7 @@ def with_raw_response(self) -> EventsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return EventsResourceWithRawResponse(self) @@ -40,7 +40,7 @@ def with_streaming_response(self) -> EventsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return EventsResourceWithStreamingResponse(self) @@ -142,7 +142,7 @@ def with_raw_response(self) -> AsyncEventsResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncEventsResourceWithRawResponse(self) @@ -151,7 +151,7 @@ def with_streaming_response(self) -> AsyncEventsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncEventsResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/messages/batch.py b/src/agentex/resources/messages/batch.py index f6c06ced..f5e61a49 100644 --- a/src/agentex/resources/messages/batch.py +++ b/src/agentex/resources/messages/batch.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> BatchResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return BatchResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> BatchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return BatchResourceWithStreamingResponse(self) @@ -131,7 +131,7 @@ def with_raw_response(self) -> AsyncBatchResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncBatchResourceWithRawResponse(self) @@ -140,7 +140,7 @@ def with_streaming_response(self) -> AsyncBatchResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncBatchResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/messages/messages.py b/src/agentex/resources/messages/messages.py index 08ebbbd1..84537bca 100644 --- a/src/agentex/resources/messages/messages.py +++ b/src/agentex/resources/messages/messages.py @@ -45,7 +45,7 @@ def with_raw_response(self) -> MessagesResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return MessagesResourceWithRawResponse(self) @@ -54,7 +54,7 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return MessagesResourceWithStreamingResponse(self) @@ -234,7 +234,7 @@ def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncMessagesResourceWithRawResponse(self) @@ -243,7 +243,7 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncMessagesResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/spans.py b/src/agentex/resources/spans.py index b896c74a..427ae079 100644 --- a/src/agentex/resources/spans.py +++ b/src/agentex/resources/spans.py @@ -32,7 +32,7 @@ def with_raw_response(self) -> SpansResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return SpansResourceWithRawResponse(self) @@ -41,7 +41,7 @@ def with_streaming_response(self) -> SpansResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return SpansResourceWithStreamingResponse(self) @@ -271,7 +271,7 @@ def with_raw_response(self) -> AsyncSpansResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncSpansResourceWithRawResponse(self) @@ -280,7 +280,7 @@ def with_streaming_response(self) -> AsyncSpansResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncSpansResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/states.py b/src/agentex/resources/states.py index d98c2ddc..8642317e 100644 --- a/src/agentex/resources/states.py +++ b/src/agentex/resources/states.py @@ -31,7 +31,7 @@ def with_raw_response(self) -> StatesResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return StatesResourceWithRawResponse(self) @@ -40,7 +40,7 @@ def with_streaming_response(self) -> StatesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return StatesResourceWithStreamingResponse(self) @@ -257,7 +257,7 @@ def with_raw_response(self) -> AsyncStatesResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncStatesResourceWithRawResponse(self) @@ -266,7 +266,7 @@ def with_streaming_response(self) -> AsyncStatesResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncStatesResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/tasks.py b/src/agentex/resources/tasks.py index 33a4648d..a941c42b 100644 --- a/src/agentex/resources/tasks.py +++ b/src/agentex/resources/tasks.py @@ -35,7 +35,7 @@ def with_raw_response(self) -> TasksResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return TasksResourceWithRawResponse(self) @@ -44,7 +44,7 @@ def with_streaming_response(self) -> TasksResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return TasksResourceWithStreamingResponse(self) @@ -318,7 +318,7 @@ def with_raw_response(self) -> AsyncTasksResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncTasksResourceWithRawResponse(self) @@ -327,7 +327,7 @@ def with_streaming_response(self) -> AsyncTasksResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncTasksResourceWithStreamingResponse(self) diff --git a/src/agentex/resources/tracker.py b/src/agentex/resources/tracker.py index 11bec36f..516e958f 100644 --- a/src/agentex/resources/tracker.py +++ b/src/agentex/resources/tracker.py @@ -31,7 +31,7 @@ def with_raw_response(self) -> TrackerResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return TrackerResourceWithRawResponse(self) @@ -40,7 +40,7 @@ def with_streaming_response(self) -> TrackerResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return TrackerResourceWithStreamingResponse(self) @@ -189,7 +189,7 @@ def with_raw_response(self) -> AsyncTrackerResourceWithRawResponse: This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. - For more information, see https://www.github.com/scaleapi/agentex-python#accessing-raw-response-data-eg-headers + For more information, see https://www.github.com/scaleapi/scale-agentex-python#accessing-raw-response-data-eg-headers """ return AsyncTrackerResourceWithRawResponse(self) @@ -198,7 +198,7 @@ def with_streaming_response(self) -> AsyncTrackerResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. - For more information, see https://www.github.com/scaleapi/agentex-python#with_streaming_response + For more information, see https://www.github.com/scaleapi/scale-agentex-python#with_streaming_response """ return AsyncTrackerResourceWithStreamingResponse(self) diff --git a/tests/test_client.py b/tests/test_client.py index da5399da..54f850e8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -59,51 +59,49 @@ def _get_open_connections(client: Agentex | AsyncAgentex) -> int: class TestAgentex: - client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - def test_raw_response(self, respx_mock: MockRouter) -> None: + def test_raw_response(self, respx_mock: MockRouter, client: Agentex) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: Agentex) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = self.client.post("/foo", cast_to=httpx.Response) + response = client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, client: Agentex) -> None: + copied = client.copy() + assert id(copied) != id(client) - copied = self.client.copy(api_key="another My API Key") + copied = client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, client: Agentex) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: client = Agentex( @@ -138,6 +136,7 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() def test_copy_default_query(self) -> None: client = Agentex( @@ -175,13 +174,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + client.close() + + def test_copy_signature(self, client: Agentex) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -192,12 +193,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, client: Agentex) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -254,14 +255,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + def test_request_timeout(self, client: Agentex) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( - FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) - ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(100.0) @@ -272,6 +271,8 @@ def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + client.close() + def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: @@ -283,6 +284,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + client.close() + # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = Agentex( @@ -293,6 +296,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + client.close() + # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = Agentex( @@ -303,6 +308,8 @@ def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + client.close() + async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: @@ -314,14 +321,14 @@ async def test_invalid_http_client(self) -> None: ) def test_default_headers_option(self) -> None: - client = Agentex( + test_client = Agentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = Agentex( + test_client2 = Agentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -330,10 +337,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + test_client.close() + test_client2.close() + def test_validate_headers(self) -> None: client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -362,8 +372,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + client.close() + + def test_request_extra_json(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -374,7 +386,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -385,7 +397,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -396,8 +408,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -407,7 +419,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -418,8 +430,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -432,7 +444,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -446,7 +458,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -489,7 +501,7 @@ def test_multipart_repeating_array(self, client: Agentex) -> None: ] @pytest.mark.respx(base_url=base_url) - def test_basic_union_response(self, respx_mock: MockRouter) -> None: + def test_basic_union_response(self, respx_mock: MockRouter, client: Agentex) -> None: class Model1(BaseModel): name: str @@ -498,12 +510,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + def test_union_response_different_types(self, respx_mock: MockRouter, client: Agentex) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -514,18 +526,18 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter, client: Agentex) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -541,7 +553,7 @@ class Model(BaseModel): ) ) - response = self.client.get("/foo", cast_to=Model) + response = client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 @@ -553,6 +565,8 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" + client.close() + def test_base_url_env(self) -> None: with update_env(AGENTEX_BASE_URL="http://localhost:5000/from/env"): client = Agentex(api_key=api_key, _strict_response_validation=True) @@ -566,6 +580,8 @@ def test_base_url_env(self) -> None: client = Agentex(base_url=None, api_key=api_key, _strict_response_validation=True, environment="production") assert str(client.base_url).startswith("http://localhost:5003") + client.close() + @pytest.mark.parametrize( "client", [ @@ -588,6 +604,7 @@ def test_base_url_trailing_slash(self, client: Agentex) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -611,6 +628,7 @@ def test_base_url_no_trailing_slash(self, client: Agentex) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + client.close() @pytest.mark.parametrize( "client", @@ -634,35 +652,36 @@ def test_absolute_request_url(self, client: Agentex) -> None: ), ) assert request.url == "https://myapi.com/foo" + client.close() def test_copied_client_does_not_close_http(self) -> None: - client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied - assert not client.is_closed() + assert not test_client.is_closed() def test_client_context_manager(self) -> None: - client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - with client as c2: - assert c2 is client + test_client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + def test_client_response_validation_error(self, respx_mock: MockRouter, client: Agentex) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - self.client.get("/foo", cast_to=Model) + client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -682,11 +701,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = client.get("/foo", cast_to=Model) + response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + strict_client.close() + non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -709,9 +731,9 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = Agentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: Agentex + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) calculated = client._calculate_retry_timeout(remaining_retries, options, headers) @@ -725,7 +747,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien with pytest.raises(APITimeoutError): client.tasks.with_streaming_response.list().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -734,7 +756,7 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client with pytest.raises(APIStatusError): client.tasks.with_streaming_response.list().__enter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -836,83 +858,77 @@ def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - def test_follow_redirects(self, respx_mock: MockRouter) -> None: + def test_follow_redirects(self, respx_mock: MockRouter, client: Agentex) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: Agentex) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - self.client.post( - "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response - ) + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" class TestAsyncAgentex: - client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response(self, respx_mock: MockRouter) -> None: + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_raw_response_for_binary(self, respx_mock: MockRouter) -> None: + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: respx_mock.post("/foo").mock( return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') ) - response = await self.client.post("/foo", cast_to=httpx.Response) + response = await async_client.post("/foo", cast_to=httpx.Response) assert response.status_code == 200 assert isinstance(response, httpx.Response) assert response.json() == {"foo": "bar"} - def test_copy(self) -> None: - copied = self.client.copy() - assert id(copied) != id(self.client) + def test_copy(self, async_client: AsyncAgentex) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) - copied = self.client.copy(api_key="another My API Key") + copied = async_client.copy(api_key="another My API Key") assert copied.api_key == "another My API Key" - assert self.client.api_key == "My API Key" + assert async_client.api_key == "My API Key" - def test_copy_default_options(self) -> None: + def test_copy_default_options(self, async_client: AsyncAgentex) -> None: # options that have a default are overridden correctly - copied = self.client.copy(max_retries=7) + copied = async_client.copy(max_retries=7) assert copied.max_retries == 7 - assert self.client.max_retries == 2 + assert async_client.max_retries == 2 copied2 = copied.copy(max_retries=6) assert copied2.max_retries == 6 assert copied.max_retries == 7 # timeout - assert isinstance(self.client.timeout, httpx.Timeout) - copied = self.client.copy(timeout=None) + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) assert copied.timeout is None - assert isinstance(self.client.timeout, httpx.Timeout) + assert isinstance(async_client.timeout, httpx.Timeout) - def test_copy_default_headers(self) -> None: + async def test_copy_default_headers(self) -> None: client = AsyncAgentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) @@ -945,8 +961,9 @@ def test_copy_default_headers(self) -> None: match="`default_headers` and `set_default_headers` arguments are mutually exclusive", ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() - def test_copy_default_query(self) -> None: + async def test_copy_default_query(self) -> None: client = AsyncAgentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) @@ -982,13 +999,15 @@ def test_copy_default_query(self) -> None: ): client.copy(set_default_query={}, default_query={"foo": "Bar"}) - def test_copy_signature(self) -> None: + await client.close() + + def test_copy_signature(self, async_client: AsyncAgentex) -> None: # ensure the same parameters that can be passed to the client are defined in the `.copy()` method init_signature = inspect.signature( # mypy doesn't like that we access the `__init__` property. - self.client.__init__, # type: ignore[misc] + async_client.__init__, # type: ignore[misc] ) - copy_signature = inspect.signature(self.client.copy) + copy_signature = inspect.signature(async_client.copy) exclude_params = {"transport", "proxies", "_strict_response_validation"} for name in init_signature.parameters.keys(): @@ -999,12 +1018,12 @@ def test_copy_signature(self) -> None: assert copy_param is not None, f"copy() signature is missing the {name} param" @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") - def test_copy_build_request(self) -> None: + def test_copy_build_request(self, async_client: AsyncAgentex) -> None: options = FinalRequestOptions(method="get", url="/foo") def build_request(options: FinalRequestOptions) -> None: - client = self.client.copy() - client._build_request(options) + client_copy = async_client.copy() + client_copy._build_request(options) # ensure that the machinery is warmed up before tracing starts. build_request(options) @@ -1061,12 +1080,12 @@ def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.Statistic print(frame) raise AssertionError() - async def test_request_timeout(self) -> None: - request = self.client._build_request(FinalRequestOptions(method="get", url="/foo")) + async def test_request_timeout(self, async_client: AsyncAgentex) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT - request = self.client._build_request( + request = async_client._build_request( FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) ) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1081,6 +1100,8 @@ async def test_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(0) + await client.close() + async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: @@ -1092,6 +1113,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == httpx.Timeout(None) + await client.close() + # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncAgentex( @@ -1102,6 +1125,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT + await client.close() + # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncAgentex( @@ -1112,6 +1137,8 @@ async def test_http_client_timeout_option(self) -> None: timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore assert timeout == DEFAULT_TIMEOUT # our default + await client.close() + def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: @@ -1122,15 +1149,15 @@ def test_invalid_http_client(self) -> None: http_client=cast(Any, http_client), ) - def test_default_headers_option(self) -> None: - client = AsyncAgentex( + async def test_default_headers_option(self) -> None: + test_client = AsyncAgentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" - client2 = AsyncAgentex( + test_client2 = AsyncAgentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, @@ -1139,10 +1166,13 @@ def test_default_headers_option(self) -> None: "X-Stainless-Lang": "my-overriding-header", }, ) - request = client2._build_request(FinalRequestOptions(method="get", url="/foo")) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + await test_client.close() + await test_client2.close() + def test_validate_headers(self) -> None: client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1153,7 +1183,7 @@ def test_validate_headers(self) -> None: client2._build_request(FinalRequestOptions(method="get", url="/foo")) - def test_default_query_option(self) -> None: + async def test_default_query_option(self) -> None: client = AsyncAgentex( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) @@ -1171,8 +1201,10 @@ def test_default_query_option(self) -> None: url = httpx.URL(request.url) assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} - def test_request_extra_json(self) -> None: - request = self.client._build_request( + await client.close() + + def test_request_extra_json(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1183,7 +1215,7 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": False} - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1194,7 +1226,7 @@ def test_request_extra_json(self) -> None: assert data == {"baz": False} # `extra_json` takes priority over `json_data` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1205,8 +1237,8 @@ def test_request_extra_json(self) -> None: data = json.loads(request.content.decode("utf-8")) assert data == {"foo": "bar", "baz": None} - def test_request_extra_headers(self) -> None: - request = self.client._build_request( + def test_request_extra_headers(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1216,7 +1248,7 @@ def test_request_extra_headers(self) -> None: assert request.headers.get("X-Foo") == "Foo" # `extra_headers` takes priority over `default_headers` when keys clash - request = self.client.with_options(default_headers={"X-Bar": "true"})._build_request( + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1227,8 +1259,8 @@ def test_request_extra_headers(self) -> None: ) assert request.headers.get("X-Bar") == "false" - def test_request_extra_query(self) -> None: - request = self.client._build_request( + def test_request_extra_query(self, client: Agentex) -> None: + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1241,7 +1273,7 @@ def test_request_extra_query(self) -> None: assert params == {"my_query_param": "Foo"} # if both `query` and `extra_query` are given, they are merged - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1255,7 +1287,7 @@ def test_request_extra_query(self) -> None: assert params == {"bar": "1", "foo": "2"} # `extra_query` takes priority over `query` when keys clash - request = self.client._build_request( + request = client._build_request( FinalRequestOptions( method="post", url="/foo", @@ -1298,7 +1330,7 @@ def test_multipart_repeating_array(self, async_client: AsyncAgentex) -> None: ] @pytest.mark.respx(base_url=base_url) - async def test_basic_union_response(self, respx_mock: MockRouter) -> None: + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: class Model1(BaseModel): name: str @@ -1307,12 +1339,12 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" @pytest.mark.respx(base_url=base_url) - async def test_union_response_different_types(self, respx_mock: MockRouter) -> None: + async def test_union_response_different_types(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: """Union of objects with the same field name using a different type""" class Model1(BaseModel): @@ -1323,18 +1355,20 @@ class Model2(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model2) assert response.foo == "bar" respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) - response = await self.client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) assert isinstance(response, Model1) assert response.foo == 1 @pytest.mark.respx(base_url=base_url) - async def test_non_application_json_content_type_for_json_data(self, respx_mock: MockRouter) -> None: + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncAgentex + ) -> None: """ Response that sets Content-Type to something other than application/json but returns json data """ @@ -1350,11 +1384,11 @@ class Model(BaseModel): ) ) - response = await self.client.get("/foo", cast_to=Model) + response = await async_client.get("/foo", cast_to=Model) assert isinstance(response, Model) assert response.foo == 2 - def test_base_url_setter(self) -> None: + async def test_base_url_setter(self) -> None: client = AsyncAgentex( base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) @@ -1364,7 +1398,9 @@ def test_base_url_setter(self) -> None: assert client.base_url == "https://example.com/from_setter/" - def test_base_url_env(self) -> None: + await client.close() + + async def test_base_url_env(self) -> None: with update_env(AGENTEX_BASE_URL="http://localhost:5000/from/env"): client = AsyncAgentex(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @@ -1379,6 +1415,8 @@ def test_base_url_env(self) -> None: ) assert str(client.base_url).startswith("http://localhost:5003") + await client.close() + @pytest.mark.parametrize( "client", [ @@ -1394,7 +1432,7 @@ def test_base_url_env(self) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_trailing_slash(self, client: AsyncAgentex) -> None: + async def test_base_url_trailing_slash(self, client: AsyncAgentex) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1403,6 +1441,7 @@ def test_base_url_trailing_slash(self, client: AsyncAgentex) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1419,7 +1458,7 @@ def test_base_url_trailing_slash(self, client: AsyncAgentex) -> None: ], ids=["standard", "custom http client"], ) - def test_base_url_no_trailing_slash(self, client: AsyncAgentex) -> None: + async def test_base_url_no_trailing_slash(self, client: AsyncAgentex) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1428,6 +1467,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncAgentex) -> None: ), ) assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() @pytest.mark.parametrize( "client", @@ -1444,7 +1484,7 @@ def test_base_url_no_trailing_slash(self, client: AsyncAgentex) -> None: ], ids=["standard", "custom http client"], ) - def test_absolute_request_url(self, client: AsyncAgentex) -> None: + async def test_absolute_request_url(self, client: AsyncAgentex) -> None: request = client._build_request( FinalRequestOptions( method="post", @@ -1453,37 +1493,37 @@ def test_absolute_request_url(self, client: AsyncAgentex) -> None: ), ) assert request.url == "https://myapi.com/foo" + await client.close() async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - assert not client.is_closed() + test_client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() - copied = client.copy() - assert copied is not client + copied = test_client.copy() + assert copied is not test_client del copied await asyncio.sleep(0.2) - assert not client.is_closed() + assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - async with client as c2: - assert c2 is client + test_client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client assert not c2.is_closed() - assert not client.is_closed() - assert client.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio - async def test_client_response_validation_error(self, respx_mock: MockRouter) -> None: + async def test_client_response_validation_error(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: class Model(BaseModel): foo: str respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) with pytest.raises(APIResponseValidationError) as exc: - await self.client.get("/foo", cast_to=Model) + await async_client.get("/foo", cast_to=Model) assert isinstance(exc.value.__cause__, ValidationError) @@ -1494,7 +1534,6 @@ async def test_client_max_retries_validation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: class Model(BaseModel): name: str @@ -1506,11 +1545,14 @@ class Model(BaseModel): with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=False) + non_strict_client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=False) - response = await client.get("/foo", cast_to=Model) + response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] + await strict_client.close() + await non_strict_client.close() + @pytest.mark.parametrize( "remaining_retries,retry_after,timeout", [ @@ -1533,13 +1575,12 @@ class Model(BaseModel): ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) - @pytest.mark.asyncio - async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncAgentex(base_url=base_url, api_key=api_key, _strict_response_validation=True) - + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncAgentex + ) -> None: headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) - calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @@ -1552,7 +1593,7 @@ async def test_retrying_timeout_errors_doesnt_leak( with pytest.raises(APITimeoutError): await async_client.tasks.with_streaming_response.list().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -1561,12 +1602,11 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, with pytest.raises(APIStatusError): await async_client.tasks.with_streaming_response.list().__aenter__() - assert _get_open_connections(self.client) == 0 + assert _get_open_connections(async_client) == 0 @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio @pytest.mark.parametrize("failure_mode", ["status", "exception"]) async def test_retries_taken( self, @@ -1598,7 +1638,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_omit_retry_count_header( self, async_client: AsyncAgentex, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1622,7 +1661,6 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("agentex._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) - @pytest.mark.asyncio async def test_overwrite_retry_count_header( self, async_client: AsyncAgentex, failures_before_success: int, respx_mock: MockRouter ) -> None: @@ -1711,26 +1749,26 @@ async def test_default_client_creation(self) -> None: ) @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: # Test that the default follow_redirects=True allows following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) - response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) assert response.status_code == 200 assert response.json() == {"status": "ok"} @pytest.mark.respx(base_url=base_url) - async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncAgentex) -> None: # Test that follow_redirects=False prevents following redirects respx_mock.post("/redirect").mock( return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) ) with pytest.raises(APIStatusError) as exc_info: - await self.client.post( + await async_client.post( "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response )