From 1d56523ed1fad60e56c773f4acd4cb478bbb31b7 Mon Sep 17 00:00:00 2001 From: Guillaume Aquilina Date: Wed, 19 Mar 2025 16:34:35 -0400 Subject: [PATCH] feat: add example about max_turns --- examples/02_agent_with_tools.py | 15 ++++ workflowai/core/domain/run.py | 23 +++-- workflowai/core/domain/run_test.py | 130 ++++++++++++++++++----------- 3 files changed, 114 insertions(+), 54 deletions(-) diff --git a/examples/02_agent_with_tools.py b/examples/02_agent_with_tools.py index 9bfb008..42679b9 100644 --- a/examples/02_agent_with_tools.py +++ b/examples/02_agent_with_tools.py @@ -11,11 +11,14 @@ from datetime import date, datetime from zoneinfo import ZoneInfo +from dotenv import load_dotenv from pydantic import BaseModel, Field import workflowai from workflowai import Model +load_dotenv(override=True) + def get_current_date() -> str: """Return today's date in ISO format (YYYY-MM-DD)""" @@ -31,6 +34,7 @@ def calculate_days_between(date1: str, date2: str) -> int: class HistoricalEventInput(BaseModel): """Input model for querying historical events.""" + query: str = Field( description="A query about a historical event", examples=[ @@ -43,6 +47,7 @@ class HistoricalEventInput(BaseModel): class HistoricalEventOutput(BaseModel): """Output model containing information about a historical event.""" + event_date: str = Field( description="The date of the event in ISO format (YYYY-MM-DD)", examples=["1969-07-20", "1945-09-02", "1776-07-04"], @@ -101,6 +106,16 @@ async def main(): ) print(run) + # Example: Make the same query but limit at a single turn to get the underlying tool call requests + print("\nExample: Latest Mars Landing") + print("-" * 50) + run = await analyze_historical_event.run( + HistoricalEventInput(query="When was the latest Mars landing?"), + max_turns=0, + max_turns_raises=False, + ) + print(run) + if __name__ == "__main__": asyncio.run(main()) diff --git a/workflowai/core/domain/run.py b/workflowai/core/domain/run.py index 80db3dc..0b4d522 100644 --- a/workflowai/core/domain/run.py +++ b/workflowai/core/domain/run.py @@ -1,3 +1,4 @@ +import json from collections.abc import Iterable from typing import Any, Generic, Optional, Protocol @@ -107,12 +108,22 @@ def format_output(self) -> str: URL: https://workflowai.com/_/agents/agent-1/runs/test-id """ # Format the output string - output = [ - "\nOutput:", - "=" * 50, - self.output.model_dump_json(indent=2), - "=" * 50, - ] + output: list[str] = [] + # In case of partial validation, it is possible that the output is an empty model + if dumped_output := self.output.model_dump(): + output += [ + "\nOutput:", + "=" * 50, + json.dumps(dumped_output, indent=2), + "=" * 50, + ] + if self.tool_call_requests: + output += [ + "\nTool Call Requests:", + "=" * 50, + json.dumps(self.model_dump(include={"tool_call_requests"})["tool_call_requests"], indent=2), + "=" * 50, + ] # Add run information if available if self.cost_usd is not None: diff --git a/workflowai/core/domain/run_test.py b/workflowai/core/domain/run_test.py index 25d4f94..0a3b413 100644 --- a/workflowai/core/domain/run_test.py +++ b/workflowai/core/domain/run_test.py @@ -8,6 +8,7 @@ Run, _AgentBase, # pyright: ignore [reportPrivateUsage] ) +from workflowai.core.domain.tool_call import ToolCallRequest from workflowai.core.domain.version import Version from workflowai.core.domain.version_properties import VersionProperties @@ -59,23 +60,29 @@ def test_different_agents(self, run1: Run[_TestOutput], run2: Run[_TestOutput]): assert run1 == run2 -# Test that format_output correctly formats: -# 1. The output as a JSON object -# 2. The cost with $ prefix and correct precision -# 3. The latency with 2 decimal places and 's' suffix -# 4. The run URL -@patch("workflowai.env.WORKFLOWAI_APP_URL", "https://workflowai.hello") -def test_format_output_full(): - run = Run[_TestOutput]( - id="run-id", - agent_id="agent-id", - schema_id=1, - output=_TestOutput(message="hello"), - duration_seconds=1.23, - cost_usd=0.001, - ) - - expected = """\nOutput: +class TestRunFormatOutput: + @pytest.fixture(autouse=True) + def mock_app_url(self): + with patch("workflowai.env.WORKFLOWAI_APP_URL", "https://workflowai.hello") as mock: + yield mock + + # Test that format_output correctly formats: + # 1. The output as a JSON object + # 2. The cost with $ prefix and correct precision + # 3. The latency with 2 decimal places and 's' suffix + # 4. The run URL + + def test_format_output_full(self): + run = Run[_TestOutput]( + id="run-id", + agent_id="agent-id", + schema_id=1, + output=_TestOutput(message="hello"), + duration_seconds=1.23, + cost_usd=0.001, + ) + + expected = """\nOutput: ================================================== { "message": "hello" @@ -85,21 +92,19 @@ def test_format_output_full(): Latency: 1.23s URL: https://workflowai.hello/_/agents/agent-id/runs/run-id""" - assert run.format_output() == expected + assert run.format_output() == expected + def test_format_output_very_low_cost(self): + run = Run[_TestOutput]( + id="run-id", + agent_id="agent-id", + schema_id=1, + output=_TestOutput(message="hello"), + duration_seconds=1.23, + cost_usd=4.97625e-05, + ) -@patch("workflowai.env.WORKFLOWAI_APP_URL", "https://workflowai.hello") -def test_format_output_very_low_cost(): - run = Run[_TestOutput]( - id="run-id", - agent_id="agent-id", - schema_id=1, - output=_TestOutput(message="hello"), - duration_seconds=1.23, - cost_usd=4.97625e-05, - ) - - expected = """\nOutput: + expected = """\nOutput: ================================================== { "message": "hello" @@ -109,23 +114,21 @@ def test_format_output_very_low_cost(): Latency: 1.23s URL: https://workflowai.hello/_/agents/agent-id/runs/run-id""" - assert run.format_output() == expected - - -# Test that format_output works correctly when cost and latency are not provided: -# 1. The output is still formatted as a JSON object -# 2. No cost or latency lines are included in the output -# 3. The run URL is still included -@patch("workflowai.env.WORKFLOWAI_APP_URL", "https://workflowai.hello") -def test_format_output_no_cost_latency(): - run = Run[_TestOutput]( - id="run-id", - agent_id="agent-id", - schema_id=1, - output=_TestOutput(message="hello"), - ) - - expected = """\nOutput: + assert run.format_output() == expected + + # Test that format_output works correctly when cost and latency are not provided: + # 1. The output is still formatted as a JSON object + # 2. No cost or latency lines are included in the output + # 3. The run URL is still included + def test_format_output_no_cost_latency(self): + run = Run[_TestOutput]( + id="run-id", + agent_id="agent-id", + schema_id=1, + output=_TestOutput(message="hello"), + ) + + expected = """\nOutput: ================================================== { "message": "hello" @@ -133,7 +136,38 @@ def test_format_output_no_cost_latency(): ================================================== URL: https://workflowai.hello/_/agents/agent-id/runs/run-id""" - assert run.format_output() == expected + assert run.format_output() == expected + + def test_format_output_tool_call_requests(self): + run = Run[_TestOutput]( + id="run-id", + agent_id="agent-id", + schema_id=1, + output=_TestOutput.model_construct(), + tool_call_requests=[ + ToolCallRequest( + id="tool-call-id", + name="tool-call-name", + input={"key": "value"}, + ), + ], + ) + assert ( + run.format_output() + == """\nTool Call Requests: +================================================== +[ + { + "id": "tool-call-id", + "name": "tool-call-name", + "input": { + "key": "value" + } + } +] +================================================== +URL: https://workflowai.hello/_/agents/agent-id/runs/run-id""" + ) class TestRunURL: