Skip to content

Commit ab5d607

Browse files
authored
Merge branch 'main' into fix/shared-computer-run-identity
2 parents 418aed3 + 0c1c7e8 commit ab5d607

65 files changed

Lines changed: 7917 additions & 243 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ INTELLIGENCE_API_KEY=
132132
# it is forwarded to the runtime; startup does not require it.
133133
COPILOTKIT_LICENSE_TOKEN=
134134

135+
# Optional Automatic Learning default. Create this container in the same Intelligence project.
136+
# Admin → Automatic Learning can enable/disable, override per Bot, and save settings in the DB.
137+
# Saved settings take precedence, including a saved off state. Unset means Learning is off.
138+
# Project credentials remain server-side; review and publish skills in the Intelligence UI.
139+
# CPK_INTELLIGENCE_LEARNING_CONTAINER_ID=support-learning
140+
# Optional opaque published revision ID; omit to follow latest.
141+
# CPK_INTELLIGENCE_SKILLS_REVISION=exact-revision-id
142+
# See docs/automatic-learning.md for managed/self-hosted setup and permissions.
143+
135144
# How long a Bot's stream may say nothing before this deployment gives up on the turn, in
136145
# milliseconds. A Bot is any AG-UI endpoint, which means it will be redeployed mid-answer, its own
137146
# upstream will time out, and it will sometimes accept a connection and then write nothing at all.

‎agent-adk/src/main.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@
1515
TOKEN_HEADER = "x-openbot-agent-token"
1616

1717

18+
class CompleteToolCallsLiteLlm(LiteLlm):
19+
"""Keep tool args together at AG-UI's long-running tool boundary.
20+
21+
ADK 2.10 streams partial FunctionCalls before their args exist. AG-UI 0.7
22+
treats the first long-running FunctionCall as complete and deduplicates the
23+
final one, losing its arguments. Wait for the complete call, while allowing
24+
ordinary text deltas to stream normally. No tool is executed here.
25+
"""
26+
27+
async def generate_content_async(self, llm_request, stream=False):
28+
async for response in super().generate_content_async(llm_request, stream):
29+
if response.partial and response.content and any(
30+
part.function_call for part in response.content.parts or []
31+
):
32+
remaining = [
33+
part for part in response.content.parts or []
34+
if not part.function_call
35+
]
36+
if not remaining:
37+
continue
38+
response = response.model_copy(update={
39+
"content": response.content.model_copy(update={"parts": remaining})
40+
})
41+
yield response
42+
43+
44+
1845
def _model_id() -> str:
1946
"""`provider/model`, which is how litellm addresses one.
2047
@@ -55,7 +82,7 @@ async def health():
5582
adk_agent=Agent(
5683
name="openbot",
5784
tools=[AGUIToolset()],
58-
model=LiteLlm(model=_model_id()),
85+
model=CompleteToolCallsLiteLlm(model=_model_id()),
5986
instruction="Answer the question you are asked, briefly and correctly.",
6087
),
6188
app_name="openbot",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-adk"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-adk", "/")
15+
16+
17+
def test_partial_tool_arguments_wait_without_dropping_neighboring_text(monkeypatch):
18+
import asyncio
19+
import importlib
20+
from google.adk.models.lite_llm import LiteLlm
21+
from google.adk.models.llm_response import LlmResponse
22+
from google.genai import types
23+
24+
monkeypatch.setenv("BOT_MODEL", "gpt-4.1-mini")
25+
main = importlib.import_module("src.main")
26+
27+
async def provider(self, request, stream=False):
28+
yield LlmResponse(partial=True, content=types.Content(role="model", parts=[
29+
types.Part(text="Looking up the policy."),
30+
types.Part(function_call=types.FunctionCall(id="call", name="copilotkit_load_skill", will_continue=True)),
31+
]))
32+
yield LlmResponse(partial=False, content=types.Content(role="model", parts=[
33+
types.Part(function_call=types.FunctionCall(id="call", name="copilotkit_load_skill", args={"skill_name": "refunds"})),
34+
]))
35+
36+
monkeypatch.setattr(LiteLlm, "generate_content_async", provider)
37+
38+
async def collect():
39+
model = main.CompleteToolCallsLiteLlm(model="openai/gpt-4.1-mini")
40+
return [response async for response in model.generate_content_async(None, True)]
41+
42+
responses = asyncio.run(collect())
43+
assert responses[0].content.parts == [types.Part(text="Looking up the policy.")]
44+
assert responses[1].content.parts[0].function_call.args == {"skill_name": "refunds"}
45+
assert responses[1].content.parts[0].function_call.id == "call"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-ag2"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-ag2", "/")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-agno"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-agno", "/agui")

‎agent-claude-sdk/tests/test_tool_continuation.py‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from src.adapter import OpenBotClaudeAgentAdapter
2828

2929

30-
def request(thread="thread-a", run="run-1", results=(), tool_count=1):
30+
def request(thread="thread-a", run="run-1", results=(), tool_count=1, tool_name="computer_navigate"):
3131
return RunAgentInput.model_validate(
3232
{
3333
"threadId": thread,
@@ -47,19 +47,24 @@ def request(thread="thread-a", run="run-1", results=(), tool_count=1):
4747
],
4848
"tools": [
4949
{
50-
"name": "computer_navigate",
50+
"name": tool_name,
5151
"description": "Navigate",
5252
"parameters": {"type": "object", "properties": {}},
5353
}
5454
],
55-
"context": [],
55+
"context": [{"description": "OpenBot learned skills", "value": "LEARNED_CATALOG_MARKER"}],
5656
"forwardedProps": {},
5757
}
5858
)
5959

6060

6161
@pytest.fixture
62-
def sdk(monkeypatch):
62+
def tool_name():
63+
return "computer_navigate"
64+
65+
66+
@pytest.fixture
67+
def sdk(monkeypatch, tool_name):
6368
clients = []
6469

6570
class Client:
@@ -106,7 +111,7 @@ async def receive_response(self):
106111
calls = [
107112
ToolUseBlock(
108113
id=f"{thread}-tool-{i}",
109-
name="mcp__ag_ui__computer_navigate",
114+
name=f"mcp__ag_ui__{tool_name}",
110115
input={},
111116
)
112117
for i in range(count)
@@ -206,15 +211,16 @@ async def events(adapter, input_data):
206211

207212
@pytest.mark.parametrize("streaming", [False, True])
208213
@pytest.mark.parametrize("model_only", [False, True])
209-
def test_client_result_resumes_original_sdk_query(sdk, streaming, model_only):
214+
@pytest.mark.parametrize("tool_name", ["computer_navigate", "copilotkit_load_skill", "copilotkit_read_skill_file"])
215+
def test_client_result_resumes_original_sdk_query(sdk, streaming, model_only, tool_name):
210216
async def scenario():
211217
adapter = OpenBotClaudeAgentAdapter(
212218
name="test",
213219
options={"include_partial_messages": streaming},
214220
model_only=model_only,
215221
)
216222
try:
217-
first = await events(adapter, request())
223+
first = await events(adapter, request(tool_name=tool_name))
218224
assert any(event.type == "TOOL_CALL_END" for event in first)
219225
opened = [
220226
event.message_id
@@ -231,6 +237,7 @@ async def scenario():
231237
adapter,
232238
request(
233239
run="run-2",
240+
tool_name=tool_name,
234241
results=[
235242
(
236243
"thread-a-tool-0",
@@ -239,6 +246,7 @@ async def scenario():
239246
],
240247
),
241248
)
249+
assert "LEARNED_CATALOG_MARKER" in str(sdk[0].options.system_prompt)
242250
assert sdk[0].queries == [("1", "thread-a")], (
243251
"a tool result must not become another SDK user prompt"
244252
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-crewai"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-crewai", "/")

‎agent-langgraph-agui/tests/test_tool_protocol.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,12 @@ async def test_a2ui_catalog_context_reaches_model_without_entering_history(bound
315315

316316

317317
@pytest.mark.asyncio
318-
@pytest.mark.parametrize("name", ["computer_navigate", "computer_run_command"])
318+
@pytest.mark.parametrize("name", ["computer_navigate", "computer_run_command", "copilotkit_load_skill", "copilotkit_read_skill_file"])
319319
async def test_surface_tool_calls_end_then_consume_actual_client_result(boundary, name):
320320
body = run_input([name])
321+
body["context"] = [{"description": "OpenBot learned skills", "value": "LEARNED_CATALOG_MARKER"}]
321322
first = await run_protocol(body)
323+
assert "LEARNED_CATALOG_MARKER" in json.dumps(boundary["model"][0]["messages"])
322324
assert [t["function"]["name"] for t in boundary["model"][0].get("tools", [])] == [
323325
name
324326
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-langroid"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-langroid", "/")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Exercise learned skill reads through the real bundled framework."""
2+
3+
import sys
4+
from pathlib import Path
5+
6+
ROOT = Path(__file__).resolve().parents[2]
7+
sys.path.insert(0, str(ROOT / "tests"))
8+
sys.path.insert(0, str(ROOT / "agent-llamaindex"))
9+
10+
from learning_harness_contract import assert_learning_delivery
11+
12+
13+
def test_learned_catalog_and_both_read_tools_reach_the_model(monkeypatch):
14+
assert_learning_delivery(monkeypatch, "agent-llamaindex", "/run")

0 commit comments

Comments
 (0)