From a25cbb95312b782850c0bf0e43a3569c6774eacc Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 8 Jun 2026 19:04:40 -0700 Subject: [PATCH] Hack temporal-direct into working Use the trick @anbuzin used for seal: pass in a dummy Provider Alternative to #151 --- examples/temporal-direct/main.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/examples/temporal-direct/main.py b/examples/temporal-direct/main.py index 9285166e..7e30a462 100644 --- a/examples/temporal-direct/main.py +++ b/examples/temporal-direct/main.py @@ -46,6 +46,29 @@ import ai +MODEL_ID = "gateway:anthropic/claude-sonnet-4.6" + + +# ── Workflow-safe model placeholder ────────────────────────────── +# +# ``agent.run`` requires a ``Model``, but a real one can't be built +# inside the workflow: ``ai.get_model("gateway:...")`` constructs an +# ``httpx.AsyncClient`` at provider-init time, which imports +# httpcore/anyio and trips the Temporal sandbox (``threading.local`` +# at module load). Our loop never calls the model directly anyway -- +# every LLM call is delegated to ``llm_call_activity``, which runs +# outside the sandbox and resolves the real model by id there. +# +# So hand the workflow a placeholder ``Model`` whose provider builds +# no client. It carries the real model id (so the activity can +# resolve it) but is safe to construct inside the sandbox. +class WorkflowModelProvider(ai.Provider[Any]): + """A clientless provider, safe to construct in a workflow sandbox.""" + + def __init__(self) -> None: + super().__init__(name="workflow-placeholder", base_url="") + + # ── Tool definitions ───────────────────────────────────────────── # # Declared with @ai.tool so the framework can extract JSON schemas @@ -93,6 +116,7 @@ async def get_population_activity(city: str) -> int: @dataclasses.dataclass class LLMParams: + model_id: str messages: list[dict[str, Any]] tool_schemas: list[dict[str, Any]] @@ -105,7 +129,7 @@ class LLMResult: @temporalio.activity.defn async def llm_call_activity(params: LLMParams) -> LLMResult: """Call the LLM, drain the stream, return the final message.""" - model = ai.get_model("gateway:anthropic/claude-sonnet-4.6") + model = ai.get_model(params.model_id) messages = [ai.messages.Message.model_validate(m) for m in params.messages] tools = [ ai.Tool( @@ -148,6 +172,7 @@ async def loop( result = await temporalio.workflow.execute_activity( llm_call_activity, LLMParams( + model_id=context.model.id, messages=[m.model_dump() for m in context.messages], tool_schemas=tool_schemas, ), @@ -213,7 +238,7 @@ async def _call() -> ai.events.ToolCallResult: class WeatherWorkflow: @temporalio.workflow.run async def run(self, user_query: str) -> str: - model = ai.get_model("gateway:anthropic/claude-sonnet-4.6") + model = ai.Model(MODEL_ID, provider=WorkflowModelProvider()) messages: list[ai.messages.Message] = [ ai.system_message( "Answer questions using the weather and population tools."