Skip to content

Commit 4b69cd4

Browse files
xumapleclaude
andauthored
AI-60: Add summary_fn parameter to TemporalModel for dynamic activity summaries (#1451)
* AI-60: Add AdkActivityConfig with summary_fn for dynamic activity summaries Introduce AdkActivityConfig extending ActivityConfig with a summary_fn field that accepts a callable for dynamic per-call summaries. When no summary_fn or static summary is set, falls back to reading adk_agent_name from LlmRequest labels for zero-config agent name display. Setting both summary and summary_fn raises ValueError to prevent ambiguity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * AI-60: Address auditor findings — determinism note, summary_fn None test, label fallback test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * AI-60: Switch to summary_fn keyword param, address auditor findings Drop AdkActivityConfig in favor of a keyword-only summary_fn parameter on TemporalModel. Zero type: ignore comments needed. Auditor findings addressed: - Label fallback test rewritten as integration test - Exception propagation documented in summary_fn docstring - Empty string summary test added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * AI-60: Qualify 'summary' as ActivityConfig summary in error message and docstring Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * AI-60: Consolidate summary tests into single workflow run Run all 4 summary_fn variants (dynamic, None, empty, label fallback) as sequential agent invocations within one workflow, reducing CI overhead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e36940 commit 4b69cd4

2 files changed

Lines changed: 128 additions & 4 deletions

File tree

temporalio/contrib/google_adk_agents/_model.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import AsyncGenerator
1+
from collections.abc import AsyncGenerator, Callable
22
from datetime import timedelta
33

44
from google.adk.models import BaseLlm, LLMRegistry
@@ -40,20 +40,37 @@ class TemporalModel(BaseLlm):
4040
"""A Temporal-based LLM model that executes model invocations as activities."""
4141

4242
def __init__(
43-
self, model_name: str, activity_config: ActivityConfig | None = None
43+
self,
44+
model_name: str,
45+
activity_config: ActivityConfig | None = None,
46+
*,
47+
summary_fn: Callable[[LlmRequest], str | None] | None = None,
4448
) -> None:
4549
"""Initialize the TemporalModel.
4650
4751
Args:
4852
model_name: The name of the model to use.
4953
activity_config: Configuration options for the activity execution.
54+
summary_fn: Optional callable that receives the LlmRequest and
55+
returns a summary string (or None) for the activity. Must be
56+
deterministic as it is called during workflow execution. If
57+
the callable raises, the exception will propagate and fail
58+
the workflow task.
59+
60+
Raises:
61+
ValueError: If both ``ActivityConfig["summary"]`` and ``summary_fn`` are set.
5062
"""
5163
super().__init__(model=model_name)
5264
self._model_name = model_name
65+
self._summary_fn = summary_fn
5366
self._activity_config = ActivityConfig(
5467
start_to_close_timeout=timedelta(seconds=60)
5568
)
56-
if activity_config:
69+
if activity_config is not None:
70+
if summary_fn is not None and activity_config.get("summary") is not None:
71+
raise ValueError(
72+
"Cannot specify both ActivityConfig 'summary' and 'summary_fn'"
73+
)
5774
self._activity_config.update(activity_config)
5875

5976
async def generate_content_async(
@@ -76,10 +93,20 @@ async def generate_content_async(
7693
yield response
7794
return
7895

96+
config = self._activity_config.copy()
97+
if self._summary_fn is not None:
98+
summary = self._summary_fn(llm_request)
99+
if summary is not None:
100+
config["summary"] = summary
101+
elif "summary" not in config:
102+
if llm_request.config and llm_request.config.labels:
103+
agent_name = llm_request.config.labels.get("adk_agent_name")
104+
if agent_name:
105+
config["summary"] = agent_name
79106
responses = await workflow.execute_activity(
80107
invoke_model,
81108
args=[llm_request],
82-
**self._activity_config,
109+
**config,
83110
)
84111
for response in responses:
85112
yield response

tests/contrib/google_adk_agents/test_google_adk_agents.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,103 @@ async def test_unsetting_timeout():
580580
assert model._activity_config.get("start_to_close_timeout", None) is None
581581

582582

583+
class SummaryFnModel(TestModel):
584+
"""Returns a single text response for summary_fn testing."""
585+
586+
def responses(self) -> list[LlmResponse]:
587+
return [
588+
LlmResponse(content=Content(role="model", parts=[Part(text="response")])),
589+
]
590+
591+
@classmethod
592+
def supported_models(cls) -> list[str]:
593+
return ["summary_fn_model"]
594+
595+
596+
@workflow.defn
597+
class SummaryTestWorkflow:
598+
@workflow.run
599+
async def run(self, model_name: str) -> None:
600+
modes = [
601+
("dynamic", lambda req: f"Invoking {req.model}"),
602+
("none", lambda req: None),
603+
("empty", lambda req: ""),
604+
("label_fallback", None),
605+
]
606+
for mode_name, summary_fn in modes:
607+
agent = Agent(
608+
name=f"summary_test_{mode_name}",
609+
model=TemporalModel(model_name, summary_fn=summary_fn),
610+
)
611+
runner = InMemoryRunner(agent=agent, app_name=f"summary_{mode_name}")
612+
session = await runner.session_service.create_session(
613+
app_name=f"summary_{mode_name}", user_id="test"
614+
)
615+
async with Aclosing(
616+
runner.run_async(
617+
user_id="test",
618+
session_id=session.id,
619+
new_message=types.Content(
620+
role="user", parts=[types.Part(text="hi")]
621+
),
622+
)
623+
) as agen:
624+
async for _ in agen:
625+
pass
626+
627+
628+
@pytest.mark.asyncio
629+
async def test_summary_fn_variants(client: Client):
630+
"""Test summary_fn with dynamic, None, empty string, and label fallback."""
631+
new_config = client.config()
632+
new_config["plugins"] = [GoogleAdkPlugin()]
633+
client = Client(**new_config)
634+
LLMRegistry.register(SummaryFnModel)
635+
636+
async with Worker(
637+
client,
638+
task_queue="adk-summary-test",
639+
workflows=[SummaryTestWorkflow],
640+
max_cached_workflows=0,
641+
):
642+
handle = await client.start_workflow(
643+
SummaryTestWorkflow.run,
644+
"summary_fn_model",
645+
id=f"summary-test-{uuid.uuid4()}",
646+
task_queue="adk-summary-test",
647+
execution_timeout=timedelta(seconds=60),
648+
)
649+
await handle.result()
650+
651+
summaries = []
652+
async for e in handle.fetch_history_events():
653+
if e.HasField("activity_task_scheduled_event_attributes"):
654+
attrs = e.activity_task_scheduled_event_attributes
655+
if attrs.activity_type.name == "invoke_model":
656+
summaries.append(e.user_metadata.summary.data)
657+
658+
assert len(summaries) == 4
659+
assert summaries[0] == b'"Invoking summary_fn_model"' # dynamic
660+
assert summaries[1] == b"" # none
661+
assert summaries[2] == b"" # empty
662+
assert (
663+
summaries[3] == b'"summary_test_label_fallback"'
664+
) # label fallback agent name
665+
666+
667+
def test_summary_and_summary_fn_raises():
668+
"""Cannot specify both summary and summary_fn."""
669+
with pytest.raises(
670+
ValueError,
671+
match="Cannot specify both ActivityConfig 'summary' and 'summary_fn'",
672+
):
673+
TemporalModel(
674+
"m",
675+
activity_config=ActivityConfig(summary="static"),
676+
summary_fn=lambda req: "dynamic",
677+
)
678+
679+
583680
@pytest.mark.asyncio
584681
async def test_agent_outside_workflow():
585682
"""Test that an agent using TemporalModel and activity_tool works outside a Temporal workflow."""

0 commit comments

Comments
 (0)