Skip to content

Commit f20257f

Browse files
Merge pull request #48 from evgeny-l/adk-middleware-instructions-provider
fix: support instructions provider for agents
2 parents 446f2e9 + 4255743 commit f20257f

File tree

2 files changed

+210
-9
lines changed

2 files changed

+210
-9
lines changed

typescript-sdk/integrations/adk-middleware/src/adk_middleware/adk_agent.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
import json
88
import asyncio
9+
import inspect
910
from datetime import datetime
1011

1112
from ag_ui.core import (
@@ -699,18 +700,41 @@ async def _start_background_execution(
699700
if input.messages and isinstance(input.messages[0], SystemMessage):
700701
system_content = input.messages[0].content
701702
if system_content:
702-
# Get existing instruction (may be None or empty)
703703
current_instruction = getattr(adk_agent, 'instruction', '') or ''
704-
705-
# Append SystemMessage content to existing instructions
706-
if current_instruction:
707-
new_instruction = f"{current_instruction}\n\n{system_content}"
704+
705+
if callable(current_instruction):
706+
# Handle instructions provider
707+
if inspect.iscoroutinefunction(current_instruction):
708+
# Async instruction provider
709+
async def instruction_provider_wrapper_async(*args, **kwargs):
710+
instructions = system_content
711+
original_instructions = await current_instruction(*args, **kwargs) or ''
712+
if original_instructions:
713+
instructions = f"{original_instructions}\n\n{instructions}"
714+
return instructions
715+
new_instruction = instruction_provider_wrapper_async
716+
else:
717+
# Sync instruction provider
718+
def instruction_provider_wrapper_sync(*args, **kwargs):
719+
instructions = system_content
720+
original_instructions = current_instruction(*args, **kwargs) or ''
721+
if original_instructions:
722+
instructions = f"{original_instructions}\n\n{instructions}"
723+
return instructions
724+
new_instruction = instruction_provider_wrapper_sync
725+
726+
logger.debug(
727+
f"Will wrap callable InstructionProvider and append SystemMessage: '{system_content[:100]}...'")
708728
else:
709-
new_instruction = system_content
710-
729+
# Handle string instructions
730+
if current_instruction:
731+
new_instruction = f"{current_instruction}\n\n{system_content}"
732+
else:
733+
new_instruction = system_content
734+
logger.debug(f"Will append SystemMessage to string instructions: '{system_content[:100]}...'")
735+
711736
agent_updates['instruction'] = new_instruction
712-
logger.debug(f"Will append SystemMessage to agent instructions: '{system_content[:100]}...'")
713-
737+
714738
# Create dynamic toolset if tools provided and prepare tool updates
715739
toolset = None
716740
if input.tools:

typescript-sdk/integrations/adk-middleware/tests/test_adk_agent.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,183 @@ async def mock_run_background(input, adk_agent, user_id, app_name, event_queue):
239239
expected_instruction = "You are a helpful assistant.\n\nBe very concise in responses."
240240
assert captured_agent.instruction == expected_instruction
241241

242+
@pytest.mark.asyncio
243+
async def test_system_message_appended_to_instruction_provider(self):
244+
"""Test that SystemMessage as first message gets appended to agent instructions
245+
when they are set via instruction provider."""
246+
# Create an agent with initial instructions
247+
received_context = None
248+
249+
async def instruction_provider(context) -> str:
250+
nonlocal received_context
251+
received_context = context
252+
return "You are a helpful assistant."
253+
254+
mock_agent = Agent(
255+
name="test_agent",
256+
instruction=instruction_provider
257+
)
258+
259+
adk_agent = ADKAgent(adk_agent=mock_agent, app_name="test_app", user_id="test_user")
260+
261+
# Create input with SystemMessage as first message
262+
system_input = RunAgentInput(
263+
thread_id="test_thread",
264+
run_id="test_run",
265+
messages=[
266+
SystemMessage(id="sys_1", role="system", content="Be very concise in responses."),
267+
UserMessage(id="msg_1", role="user", content="Hello")
268+
],
269+
context=[],
270+
state={},
271+
tools=[],
272+
forwarded_props={}
273+
)
274+
275+
# Mock the background execution to capture the modified agent
276+
captured_agent = None
277+
original_run_background = adk_agent._run_adk_in_background
278+
279+
async def mock_run_background(input, adk_agent, user_id, app_name, event_queue):
280+
nonlocal captured_agent
281+
captured_agent = adk_agent
282+
# Just put a completion event in the queue and return
283+
await event_queue.put(None)
284+
285+
with patch.object(adk_agent, '_run_adk_in_background', side_effect=mock_run_background):
286+
# Start execution to trigger agent modification
287+
execution = await adk_agent._start_background_execution(system_input)
288+
289+
# Wait briefly for the background task to start
290+
await asyncio.sleep(0.01)
291+
292+
# Verify the agent's instruction was wrapped correctly
293+
assert captured_agent is not None
294+
assert callable(captured_agent.instruction) is True
295+
296+
# Test that the context object received in instruction provider is the same
297+
test_context = {"test": "value"}
298+
expected_instruction = "You are a helpful assistant.\n\nBe very concise in responses."
299+
agent_instruction = await captured_agent.instruction(test_context)
300+
assert agent_instruction == expected_instruction
301+
assert received_context is test_context
302+
303+
@pytest.mark.asyncio
304+
async def test_system_message_appended_to_instruction_provider_with_none(self):
305+
"""Test that SystemMessage as first message gets appended to agent instructions
306+
when they are set via instruction provider."""
307+
# Create an agent with initial instructions, but return None
308+
async def instruction_provider(context) -> str:
309+
return None
310+
311+
mock_agent = Agent(
312+
name="test_agent",
313+
instruction=instruction_provider
314+
)
315+
316+
adk_agent = ADKAgent(adk_agent=mock_agent, app_name="test_app", user_id="test_user")
317+
318+
# Create input with SystemMessage as first message
319+
system_input = RunAgentInput(
320+
thread_id="test_thread",
321+
run_id="test_run",
322+
messages=[
323+
SystemMessage(id="sys_1", role="system", content="Be very concise in responses."),
324+
UserMessage(id="msg_1", role="user", content="Hello")
325+
],
326+
context=[],
327+
state={},
328+
tools=[],
329+
forwarded_props={}
330+
)
331+
332+
# Mock the background execution to capture the modified agent
333+
captured_agent = None
334+
original_run_background = adk_agent._run_adk_in_background
335+
336+
async def mock_run_background(input, adk_agent, user_id, app_name, event_queue):
337+
nonlocal captured_agent
338+
captured_agent = adk_agent
339+
# Just put a completion event in the queue and return
340+
await event_queue.put(None)
341+
342+
with patch.object(adk_agent, '_run_adk_in_background', side_effect=mock_run_background):
343+
# Start execution to trigger agent modification
344+
execution = await adk_agent._start_background_execution(system_input)
345+
346+
# Wait briefly for the background task to start
347+
await asyncio.sleep(0.01)
348+
349+
# Verify the agent's instruction was wrapped correctly
350+
assert captured_agent is not None
351+
assert callable(captured_agent.instruction) is True
352+
353+
# No empty new lines should be added before the instructions
354+
expected_instruction = "Be very concise in responses."
355+
agent_instruction = await captured_agent.instruction({})
356+
assert agent_instruction == expected_instruction
357+
358+
@pytest.mark.asyncio
359+
async def test_system_message_appended_to_sync_instruction_provider(self):
360+
"""Test that SystemMessage as first message gets appended to agent instructions
361+
when they are set via sync instruction provider."""
362+
# Create an agent with initial instructions
363+
received_context = None
364+
365+
def instruction_provider(context) -> str:
366+
nonlocal received_context
367+
received_context = context
368+
return "You are a helpful assistant."
369+
370+
mock_agent = Agent(
371+
name="test_agent",
372+
instruction=instruction_provider
373+
)
374+
375+
adk_agent = ADKAgent(adk_agent=mock_agent, app_name="test_app", user_id="test_user")
376+
377+
# Create input with SystemMessage as first message
378+
system_input = RunAgentInput(
379+
thread_id="test_thread",
380+
run_id="test_run",
381+
messages=[
382+
SystemMessage(id="sys_1", role="system", content="Be very concise in responses."),
383+
UserMessage(id="msg_1", role="user", content="Hello")
384+
],
385+
context=[],
386+
state={},
387+
tools=[],
388+
forwarded_props={}
389+
)
390+
391+
# Mock the background execution to capture the modified agent
392+
captured_agent = None
393+
original_run_background = adk_agent._run_adk_in_background
394+
395+
async def mock_run_background(input, adk_agent, user_id, app_name, event_queue):
396+
nonlocal captured_agent
397+
captured_agent = adk_agent
398+
# Just put a completion event in the queue and return
399+
await event_queue.put(None)
400+
401+
with patch.object(adk_agent, '_run_adk_in_background', side_effect=mock_run_background):
402+
# Start execution to trigger agent modification
403+
execution = await adk_agent._start_background_execution(system_input)
404+
405+
# Wait briefly for the background task to start
406+
await asyncio.sleep(0.01)
407+
408+
# Verify agent was captured
409+
assert captured_agent is not None
410+
assert callable(captured_agent.instruction)
411+
412+
# Test that the context object received in instruction provider is the same
413+
test_context = {"test": "value"}
414+
expected_instruction = "You are a helpful assistant.\n\nBe very concise in responses."
415+
agent_instruction = captured_agent.instruction(test_context) # Note: no await for sync function
416+
assert agent_instruction == expected_instruction
417+
assert received_context is test_context
418+
242419
@pytest.mark.asyncio
243420
async def test_system_message_not_first_ignored(self):
244421
"""Test that SystemMessage not as first message is ignored."""

0 commit comments

Comments
 (0)