kagent-adk 0.6.6__tar.gz → 0.6.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kagent-adk might be problematic. Click here for more details.
- {kagent_adk-0.6.6 → kagent_adk-0.6.8}/PKG-INFO +2 -7
- {kagent_adk-0.6.6 → kagent_adk-0.6.8}/pyproject.toml +7 -9
- {kagent_adk-0.6.6/src/kagent_adk → kagent_adk-0.6.8/src/kagent/adk}/__init__.py +1 -1
- kagent_adk-0.6.6/src/kagent_adk/a2a.py → kagent_adk-0.6.8/src/kagent/adk/_a2a.py +6 -51
- {kagent_adk-0.6.6/src/kagent_adk → kagent_adk-0.6.8/src/kagent/adk}/_agent_executor.py +19 -8
- {kagent_adk-0.6.6/src/kagent_adk → kagent_adk-0.6.8/src/kagent/adk}/_token.py +3 -1
- kagent_adk-0.6.8/src/kagent/adk/cli.py +111 -0
- kagent_adk-0.6.8/src/kagent/adk/converters/__init__.py +0 -0
- kagent_adk-0.6.8/src/kagent/adk/converters/event_converter.py +315 -0
- kagent_adk-0.6.8/src/kagent/adk/converters/part_converter.py +206 -0
- kagent_adk-0.6.8/src/kagent/adk/converters/request_converter.py +33 -0
- {kagent_adk-0.6.6/src/kagent_adk → kagent_adk-0.6.8/src/kagent/adk}/models.py +7 -14
- kagent_adk-0.6.6/src/kagent_adk/_task_store.py +0 -30
- kagent_adk-0.6.6/src/kagent_adk/cli.py +0 -204
- {kagent_adk-0.6.6 → kagent_adk-0.6.8}/.gitignore +0 -0
- {kagent_adk-0.6.6 → kagent_adk-0.6.8}/.python-version +0 -0
- {kagent_adk-0.6.6 → kagent_adk-0.6.8}/README.md +0 -0
- {kagent_adk-0.6.6/src/kagent_adk → kagent_adk-0.6.8/src/kagent/adk}/_session_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kagent-adk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.8
|
|
4
4
|
Summary: kagent-adk is an sdk for integrating adk agents with kagent
|
|
5
5
|
Requires-Python: >=3.12.11
|
|
6
6
|
Requires-Dist: a2a-sdk>=0.3.1
|
|
@@ -13,15 +13,10 @@ Requires-Dist: google-auth>=2.40.2
|
|
|
13
13
|
Requires-Dist: google-genai>=1.21.1
|
|
14
14
|
Requires-Dist: httpx>=0.25.0
|
|
15
15
|
Requires-Dist: jsonref>=1.1.0
|
|
16
|
+
Requires-Dist: kagent-core
|
|
16
17
|
Requires-Dist: litellm>=1.74.3
|
|
17
18
|
Requires-Dist: mcp>=1.12.0
|
|
18
19
|
Requires-Dist: openai>=1.72.0
|
|
19
|
-
Requires-Dist: opentelemetry-api>=1.36.0
|
|
20
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.36.0
|
|
21
|
-
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.44.0
|
|
22
|
-
Requires-Dist: opentelemetry-instrumentation-httpx>=0.52.0
|
|
23
|
-
Requires-Dist: opentelemetry-instrumentation-openai>=0.44.3
|
|
24
|
-
Requires-Dist: opentelemetry-sdk>=1.36.0
|
|
25
20
|
Requires-Dist: protobuf>=6
|
|
26
21
|
Requires-Dist: pydantic>=2.5.0
|
|
27
22
|
Requires-Dist: typer>=0.15.0
|
|
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kagent-adk"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.8"
|
|
8
8
|
description = "kagent-adk is an sdk for integrating adk agents with kagent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12.11"
|
|
11
11
|
dependencies = [
|
|
12
|
+
"kagent-core",
|
|
12
13
|
"aiofiles>=24.1.0",
|
|
13
14
|
"anyio>=4.9.0",
|
|
14
15
|
"typer>=0.15.0",
|
|
@@ -16,12 +17,6 @@ dependencies = [
|
|
|
16
17
|
"openai>=1.72.0",
|
|
17
18
|
"mcp>=1.12.0",
|
|
18
19
|
"protobuf>=6",
|
|
19
|
-
"opentelemetry-api>=1.36.0",
|
|
20
|
-
"opentelemetry-sdk>=1.36.0",
|
|
21
|
-
"opentelemetry-exporter-otlp-proto-grpc>=1.36.0",
|
|
22
|
-
"opentelemetry-instrumentation-openai>= 0.44.3",
|
|
23
|
-
"opentelemetry-instrumentation-anthropic>=0.44.0",
|
|
24
|
-
"opentelemetry-instrumentation-httpx >= 0.52.0",
|
|
25
20
|
"anthropic[vertex]>=0.49.0",
|
|
26
21
|
"fastapi>=0.115.1",
|
|
27
22
|
"litellm>=1.74.3",
|
|
@@ -35,8 +30,11 @@ dependencies = [
|
|
|
35
30
|
"a2a-sdk>=0.3.1",
|
|
36
31
|
]
|
|
37
32
|
|
|
33
|
+
[tool.uv.sources]
|
|
34
|
+
kagent-core = {workspace = true}
|
|
35
|
+
|
|
38
36
|
[project.scripts]
|
|
39
|
-
kagent-adk = "
|
|
37
|
+
kagent-adk = "kagent.adk.cli:app"
|
|
40
38
|
|
|
41
39
|
[project.optional-dependencies]
|
|
42
40
|
test = [
|
|
@@ -48,7 +46,7 @@ memory = [
|
|
|
48
46
|
]
|
|
49
47
|
|
|
50
48
|
[tool.hatch.build.targets.wheel]
|
|
51
|
-
packages = ["src/
|
|
49
|
+
packages = ["src/kagent"]
|
|
52
50
|
|
|
53
51
|
[tool.ruff]
|
|
54
52
|
extend = "../../pyproject.toml"
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
#! /usr/bin/env python3
|
|
2
2
|
import faulthandler
|
|
3
|
-
import inspect
|
|
4
3
|
import logging
|
|
5
4
|
import os
|
|
6
|
-
import
|
|
7
|
-
from contextlib import asynccontextmanager
|
|
8
|
-
from typing import Awaitable, Callable, override
|
|
5
|
+
from typing import Callable
|
|
9
6
|
|
|
10
7
|
import httpx
|
|
11
|
-
from a2a.
|
|
12
|
-
from a2a.server.agent_execution import RequestContext, SimpleRequestContextBuilder
|
|
13
|
-
from a2a.server.apps import A2AStarletteApplication
|
|
14
|
-
from a2a.server.context import ServerCallContext
|
|
8
|
+
from a2a.server.apps import A2AFastAPIApplication
|
|
15
9
|
from a2a.server.request_handlers import DefaultRequestHandler
|
|
16
|
-
from a2a.
|
|
17
|
-
from a2a.types import AgentCard, MessageSendParams, Task
|
|
10
|
+
from a2a.types import AgentCard
|
|
18
11
|
from fastapi import FastAPI, Request
|
|
19
12
|
from fastapi.responses import PlainTextResponse
|
|
20
13
|
from google.adk.agents import BaseAgent
|
|
@@ -22,54 +15,16 @@ from google.adk.runners import Runner
|
|
|
22
15
|
from google.adk.sessions import InMemorySessionService
|
|
23
16
|
from google.genai import types
|
|
24
17
|
|
|
18
|
+
from kagent.core.a2a import KAgentRequestContextBuilder, KAgentTaskStore
|
|
19
|
+
|
|
25
20
|
from ._agent_executor import A2aAgentExecutor
|
|
26
21
|
from ._session_service import KAgentSessionService
|
|
27
|
-
from ._task_store import KAgentTaskStore
|
|
28
22
|
from ._token import KAgentTokenService
|
|
29
23
|
|
|
30
24
|
# --- Configure Logging ---
|
|
31
25
|
logger = logging.getLogger(__name__)
|
|
32
26
|
|
|
33
27
|
|
|
34
|
-
class KAgentUser(User):
|
|
35
|
-
def __init__(self, user_id: str):
|
|
36
|
-
self.user_id = user_id
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def is_authenticated(self) -> bool:
|
|
40
|
-
return True
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def user_name(self) -> str:
|
|
44
|
-
return self.user_id
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class KAgentRequestContextBuilder(SimpleRequestContextBuilder):
|
|
48
|
-
"""
|
|
49
|
-
A request context builder that will be used to hack in the user_id for now.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def __init__(self, task_store: TaskStore):
|
|
53
|
-
super().__init__(task_store=task_store)
|
|
54
|
-
|
|
55
|
-
async def build(
|
|
56
|
-
self,
|
|
57
|
-
params: MessageSendParams | None = None,
|
|
58
|
-
task_id: str | None = None,
|
|
59
|
-
context_id: str | None = None,
|
|
60
|
-
task: Task | None = None,
|
|
61
|
-
context: ServerCallContext | None = None,
|
|
62
|
-
) -> RequestContext:
|
|
63
|
-
if context:
|
|
64
|
-
# grab the user id from the header
|
|
65
|
-
headers = context.state.get("headers", {})
|
|
66
|
-
user_id = headers.get("x-user-id", None)
|
|
67
|
-
if user_id:
|
|
68
|
-
context.user = KAgentUser(user_id=user_id)
|
|
69
|
-
request_context = await super().build(params, task_id, context_id, task, context)
|
|
70
|
-
return request_context
|
|
71
|
-
|
|
72
|
-
|
|
73
28
|
def health_check(request: Request) -> PlainTextResponse:
|
|
74
29
|
return PlainTextResponse("OK")
|
|
75
30
|
|
|
@@ -126,7 +81,7 @@ class KAgentApp:
|
|
|
126
81
|
request_context_builder=request_context_builder,
|
|
127
82
|
)
|
|
128
83
|
|
|
129
|
-
a2a_app =
|
|
84
|
+
a2a_app = A2AFastAPIApplication(
|
|
130
85
|
agent_card=self.agent_card,
|
|
131
86
|
http_handler=request_handler,
|
|
132
87
|
)
|
|
@@ -13,6 +13,7 @@ from a2a.server.events.event_queue import EventQueue
|
|
|
13
13
|
from a2a.types import (
|
|
14
14
|
Artifact,
|
|
15
15
|
Message,
|
|
16
|
+
Part,
|
|
16
17
|
Role,
|
|
17
18
|
TaskArtifactUpdateEvent,
|
|
18
19
|
TaskState,
|
|
@@ -20,14 +21,16 @@ from a2a.types import (
|
|
|
20
21
|
TaskStatusUpdateEvent,
|
|
21
22
|
TextPart,
|
|
22
23
|
)
|
|
23
|
-
from google.adk.a2a.converters.event_converter import convert_event_to_a2a_events
|
|
24
|
-
from google.adk.a2a.converters.request_converter import convert_a2a_request_to_adk_run_args
|
|
25
|
-
from google.adk.a2a.converters.utils import _get_adk_metadata_key
|
|
26
|
-
from google.adk.a2a.executor.task_result_aggregator import TaskResultAggregator
|
|
27
24
|
from google.adk.runners import Runner
|
|
25
|
+
from opentelemetry import trace
|
|
28
26
|
from pydantic import BaseModel
|
|
29
27
|
from typing_extensions import override
|
|
30
28
|
|
|
29
|
+
from kagent.core.a2a import TaskResultAggregator, get_kagent_metadata_key
|
|
30
|
+
|
|
31
|
+
from .converters.event_converter import convert_event_to_a2a_events
|
|
32
|
+
from .converters.request_converter import convert_a2a_request_to_adk_run_args
|
|
33
|
+
|
|
31
34
|
logger = logging.getLogger("google_adk." + __name__)
|
|
32
35
|
|
|
33
36
|
|
|
@@ -133,7 +136,7 @@ class A2aAgentExecutor(AgentExecutor):
|
|
|
133
136
|
message=Message(
|
|
134
137
|
message_id=str(uuid.uuid4()),
|
|
135
138
|
role=Role.agent,
|
|
136
|
-
parts=[TextPart(text=str(e))],
|
|
139
|
+
parts=[Part(TextPart(text=str(e)))],
|
|
137
140
|
),
|
|
138
141
|
),
|
|
139
142
|
context_id=context.context_id,
|
|
@@ -166,6 +169,14 @@ class A2aAgentExecutor(AgentExecutor):
|
|
|
166
169
|
# ensure the session exists
|
|
167
170
|
session = await self._prepare_session(context, run_args, runner)
|
|
168
171
|
|
|
172
|
+
current_span = trace.get_current_span()
|
|
173
|
+
if run_args["user_id"]:
|
|
174
|
+
current_span.set_attribute("kagent.user_id", run_args["user_id"])
|
|
175
|
+
if context.task_id:
|
|
176
|
+
current_span.set_attribute("gen_ai.task.id", context.task_id)
|
|
177
|
+
if run_args["session_id"]:
|
|
178
|
+
current_span.set_attribute("gen_ai.converstation.id", run_args["session_id"])
|
|
179
|
+
|
|
169
180
|
# create invocation context
|
|
170
181
|
invocation_context = runner._new_invocation_context(
|
|
171
182
|
session=session,
|
|
@@ -184,9 +195,9 @@ class A2aAgentExecutor(AgentExecutor):
|
|
|
184
195
|
context_id=context.context_id,
|
|
185
196
|
final=False,
|
|
186
197
|
metadata={
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
get_kagent_metadata_key("app_name"): runner.app_name,
|
|
199
|
+
get_kagent_metadata_key("user_id"): run_args["user_id"],
|
|
200
|
+
get_kagent_metadata_key("session_id"): run_args["session_id"],
|
|
190
201
|
},
|
|
191
202
|
)
|
|
192
203
|
)
|
|
@@ -3,6 +3,8 @@ import asyncio
|
|
|
3
3
|
from contextlib import asynccontextmanager
|
|
4
4
|
from typing import Any, Optional
|
|
5
5
|
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
6
8
|
KAGENT_TOKEN_PATH = "/var/run/secrets/tokens/kagent-token"
|
|
7
9
|
logger = logging.getLogger(__name__)
|
|
8
10
|
|
|
@@ -59,7 +61,7 @@ class KAgentTokenService:
|
|
|
59
61
|
async with self.update_lock:
|
|
60
62
|
self.token = token
|
|
61
63
|
|
|
62
|
-
async def _add_bearer_token(self, request):
|
|
64
|
+
async def _add_bearer_token(self, request: httpx.Request):
|
|
63
65
|
# Your function to generate headers dynamically
|
|
64
66
|
token = await self._get_token()
|
|
65
67
|
headers = {"X-Agent-Name": self.app_name}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import uvicorn
|
|
9
|
+
from a2a.types import AgentCard
|
|
10
|
+
from google.adk.cli.utils.agent_loader import AgentLoader
|
|
11
|
+
|
|
12
|
+
from kagent.core import KAgentConfig, configure_tracing
|
|
13
|
+
|
|
14
|
+
from . import AgentConfig, KAgentApp
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
app = typer.Typer()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def static(
|
|
23
|
+
host: str = "127.0.0.1",
|
|
24
|
+
port: int = 8080,
|
|
25
|
+
workers: int = 1,
|
|
26
|
+
filepath: str = "/config",
|
|
27
|
+
reload: Annotated[bool, typer.Option("--reload")] = False,
|
|
28
|
+
):
|
|
29
|
+
app_cfg = KAgentConfig()
|
|
30
|
+
|
|
31
|
+
with open(os.path.join(filepath, "config.json"), "r") as f:
|
|
32
|
+
config = json.load(f)
|
|
33
|
+
agent_config = AgentConfig.model_validate(config)
|
|
34
|
+
with open(os.path.join(filepath, "agent-card.json"), "r") as f:
|
|
35
|
+
agent_card = json.load(f)
|
|
36
|
+
agent_card = AgentCard.model_validate(agent_card)
|
|
37
|
+
root_agent = agent_config.to_agent(app_cfg.name)
|
|
38
|
+
|
|
39
|
+
kagent_app = KAgentApp(root_agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
40
|
+
|
|
41
|
+
server = kagent_app.build()
|
|
42
|
+
configure_tracing(server)
|
|
43
|
+
|
|
44
|
+
uvicorn.run(
|
|
45
|
+
server,
|
|
46
|
+
host=host,
|
|
47
|
+
port=port,
|
|
48
|
+
workers=workers,
|
|
49
|
+
reload=reload,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command()
|
|
54
|
+
def run(
|
|
55
|
+
name: Annotated[str, typer.Argument(help="The name of the agent to run")],
|
|
56
|
+
working_dir: str = ".",
|
|
57
|
+
host: str = "127.0.0.1",
|
|
58
|
+
port: int = 8080,
|
|
59
|
+
workers: int = 1,
|
|
60
|
+
):
|
|
61
|
+
app_cfg = KAgentConfig()
|
|
62
|
+
|
|
63
|
+
agent_loader = AgentLoader(agents_dir=working_dir)
|
|
64
|
+
root_agent = agent_loader.load_agent(name)
|
|
65
|
+
|
|
66
|
+
with open(os.path.join(working_dir, name, "agent-card.json"), "r") as f:
|
|
67
|
+
agent_card = json.load(f)
|
|
68
|
+
agent_card = AgentCard.model_validate(agent_card)
|
|
69
|
+
kagent_app = KAgentApp(root_agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
70
|
+
server = kagent_app.build()
|
|
71
|
+
configure_tracing(server)
|
|
72
|
+
|
|
73
|
+
uvicorn.run(
|
|
74
|
+
server,
|
|
75
|
+
host=host,
|
|
76
|
+
port=port,
|
|
77
|
+
workers=workers,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_agent(agent_config: AgentConfig, agent_card: AgentCard, task: str):
|
|
82
|
+
app_cfg = KAgentConfig()
|
|
83
|
+
agent = agent_config.to_agent(app_cfg.name)
|
|
84
|
+
app = KAgentApp(agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
85
|
+
await app.test(task)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def test(
|
|
90
|
+
task: Annotated[str, typer.Option("--task", help="The task to test the agent with")],
|
|
91
|
+
filepath: Annotated[str, typer.Option("--filepath", help="The path to the agent config file")],
|
|
92
|
+
):
|
|
93
|
+
with open(filepath, "r") as f:
|
|
94
|
+
content = f.read()
|
|
95
|
+
config = json.loads(content)
|
|
96
|
+
|
|
97
|
+
with open(os.path.join(filepath, "agent-card.json"), "r") as f:
|
|
98
|
+
agent_card = json.load(f)
|
|
99
|
+
agent_card = AgentCard.model_validate(agent_card)
|
|
100
|
+
agent_config = AgentConfig.model_validate(config)
|
|
101
|
+
asyncio.run(test_agent(agent_config, agent_card, task))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_cli():
|
|
105
|
+
logging.basicConfig(level=logging.INFO)
|
|
106
|
+
logging.info("Starting KAgent")
|
|
107
|
+
app()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
run_cli()
|
|
File without changes
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from a2a.server.events import Event as A2AEvent
|
|
9
|
+
from a2a.types import DataPart, Message, Role, Task, TaskState, TaskStatus, TaskStatusUpdateEvent, TextPart
|
|
10
|
+
from a2a.types import Part as A2APart
|
|
11
|
+
from google.adk.agents.invocation_context import InvocationContext
|
|
12
|
+
from google.adk.events.event import Event
|
|
13
|
+
from google.adk.flows.llm_flows.functions import REQUEST_EUC_FUNCTION_CALL_NAME
|
|
14
|
+
from google.genai import types as genai_types
|
|
15
|
+
|
|
16
|
+
from kagent.core.a2a import (
|
|
17
|
+
A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY,
|
|
18
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
19
|
+
A2A_DATA_PART_METADATA_TYPE_KEY,
|
|
20
|
+
get_kagent_metadata_key,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .part_converter import (
|
|
24
|
+
convert_genai_part_to_a2a_part,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Constants
|
|
28
|
+
|
|
29
|
+
ARTIFACT_ID_SEPARATOR = "-"
|
|
30
|
+
DEFAULT_ERROR_MESSAGE = "An error occurred during processing"
|
|
31
|
+
|
|
32
|
+
# Logger
|
|
33
|
+
logger = logging.getLogger("kagent_adk." + __name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _serialize_metadata_value(value: Any) -> str:
|
|
37
|
+
"""Safely serializes metadata values to string format.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
value: The value to serialize.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
String representation of the value.
|
|
44
|
+
"""
|
|
45
|
+
if hasattr(value, "model_dump"):
|
|
46
|
+
try:
|
|
47
|
+
return value.model_dump(exclude_none=True, by_alias=True)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.warning("Failed to serialize metadata value: %s", e)
|
|
50
|
+
return str(value)
|
|
51
|
+
return str(value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_context_metadata(event: Event, invocation_context: InvocationContext) -> Dict[str, str]:
|
|
55
|
+
"""Gets the context metadata for the event.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
event: The ADK event to extract metadata from.
|
|
59
|
+
invocation_context: The invocation context containing session information.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A dictionary containing the context metadata.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If required fields are missing from event or context.
|
|
66
|
+
"""
|
|
67
|
+
if not event:
|
|
68
|
+
raise ValueError("Event cannot be None")
|
|
69
|
+
if not invocation_context:
|
|
70
|
+
raise ValueError("Invocation context cannot be None")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
metadata = {
|
|
74
|
+
get_kagent_metadata_key("app_name"): invocation_context.app_name,
|
|
75
|
+
get_kagent_metadata_key("user_id"): invocation_context.user_id,
|
|
76
|
+
get_kagent_metadata_key("session_id"): invocation_context.session.id,
|
|
77
|
+
get_kagent_metadata_key("invocation_id"): event.invocation_id,
|
|
78
|
+
get_kagent_metadata_key("author"): event.author,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Add optional metadata fields if present
|
|
82
|
+
optional_fields = [
|
|
83
|
+
("branch", event.branch),
|
|
84
|
+
("grounding_metadata", event.grounding_metadata),
|
|
85
|
+
("custom_metadata", event.custom_metadata),
|
|
86
|
+
("usage_metadata", event.usage_metadata),
|
|
87
|
+
("error_code", event.error_code),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for field_name, field_value in optional_fields:
|
|
91
|
+
if field_value is not None:
|
|
92
|
+
metadata[get_kagent_metadata_key(field_name)] = _serialize_metadata_value(field_value)
|
|
93
|
+
|
|
94
|
+
return metadata
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("Failed to create context metadata: %s", e)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _create_artifact_id(app_name: str, user_id: str, session_id: str, filename: str, version: int) -> str:
|
|
102
|
+
"""Creates a unique artifact ID.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
app_name: The application name.
|
|
106
|
+
user_id: The user ID.
|
|
107
|
+
session_id: The session ID.
|
|
108
|
+
filename: The artifact filename.
|
|
109
|
+
version: The artifact version.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A unique artifact ID string.
|
|
113
|
+
"""
|
|
114
|
+
components = [app_name, user_id, session_id, filename, str(version)]
|
|
115
|
+
return ARTIFACT_ID_SEPARATOR.join(components)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _process_long_running_tool(a2a_part: A2APart, event: Event) -> None:
|
|
119
|
+
"""Processes long-running tool metadata for an A2A part.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
a2a_part: The A2A part to potentially mark as long-running.
|
|
123
|
+
event: The ADK event containing long-running tool information.
|
|
124
|
+
"""
|
|
125
|
+
if (
|
|
126
|
+
isinstance(a2a_part.root, DataPart)
|
|
127
|
+
and event.long_running_tool_ids
|
|
128
|
+
and a2a_part.root.metadata
|
|
129
|
+
and a2a_part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
130
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
131
|
+
and a2a_part.root.data.get("id") in event.long_running_tool_ids
|
|
132
|
+
):
|
|
133
|
+
a2a_part.root.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)] = True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def convert_event_to_a2a_message(
|
|
137
|
+
event: Event, invocation_context: InvocationContext, role: Role = Role.agent
|
|
138
|
+
) -> Optional[Message]:
|
|
139
|
+
"""Converts an ADK event to an A2A message.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
event: The ADK event to convert.
|
|
143
|
+
invocation_context: The invocation context.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
An A2A Message if the event has content, None otherwise.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If required parameters are invalid.
|
|
150
|
+
"""
|
|
151
|
+
if not event:
|
|
152
|
+
raise ValueError("Event cannot be None")
|
|
153
|
+
if not invocation_context:
|
|
154
|
+
raise ValueError("Invocation context cannot be None")
|
|
155
|
+
|
|
156
|
+
if not event.content or not event.content.parts:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
a2a_parts = []
|
|
161
|
+
for part in event.content.parts:
|
|
162
|
+
a2a_part = convert_genai_part_to_a2a_part(part)
|
|
163
|
+
if a2a_part:
|
|
164
|
+
a2a_parts.append(a2a_part)
|
|
165
|
+
_process_long_running_tool(a2a_part, event)
|
|
166
|
+
|
|
167
|
+
if a2a_parts:
|
|
168
|
+
return Message(message_id=str(uuid.uuid4()), role=role, parts=a2a_parts)
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error("Failed to convert event to status message: %s", e)
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _create_error_status_event(
|
|
178
|
+
event: Event,
|
|
179
|
+
invocation_context: InvocationContext,
|
|
180
|
+
task_id: Optional[str] = None,
|
|
181
|
+
context_id: Optional[str] = None,
|
|
182
|
+
) -> TaskStatusUpdateEvent:
|
|
183
|
+
"""Creates a TaskStatusUpdateEvent for error scenarios.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
event: The ADK event containing error information.
|
|
187
|
+
invocation_context: The invocation context.
|
|
188
|
+
task_id: Optional task ID to use for generated events.
|
|
189
|
+
context_id: Optional Context ID to use for generated events.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A TaskStatusUpdateEvent with FAILED state.
|
|
193
|
+
"""
|
|
194
|
+
error_message = getattr(event, "error_message", None) or DEFAULT_ERROR_MESSAGE
|
|
195
|
+
|
|
196
|
+
# Get context metadata and add error code
|
|
197
|
+
event_metadata = _get_context_metadata(event, invocation_context)
|
|
198
|
+
if event.error_code:
|
|
199
|
+
event_metadata[get_kagent_metadata_key("error_code")] = str(event.error_code)
|
|
200
|
+
|
|
201
|
+
return TaskStatusUpdateEvent(
|
|
202
|
+
task_id=task_id,
|
|
203
|
+
context_id=context_id,
|
|
204
|
+
metadata=event_metadata,
|
|
205
|
+
status=TaskStatus(
|
|
206
|
+
state=TaskState.failed,
|
|
207
|
+
message=Message(
|
|
208
|
+
message_id=str(uuid.uuid4()),
|
|
209
|
+
role=Role.agent,
|
|
210
|
+
parts=[A2APart(TextPart(text=error_message))],
|
|
211
|
+
metadata={get_kagent_metadata_key("error_code"): str(event.error_code)} if event.error_code else {},
|
|
212
|
+
),
|
|
213
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
214
|
+
),
|
|
215
|
+
final=False,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _create_status_update_event(
|
|
220
|
+
message: Message,
|
|
221
|
+
invocation_context: InvocationContext,
|
|
222
|
+
event: Event,
|
|
223
|
+
task_id: Optional[str] = None,
|
|
224
|
+
context_id: Optional[str] = None,
|
|
225
|
+
) -> TaskStatusUpdateEvent:
|
|
226
|
+
"""Creates a TaskStatusUpdateEvent for running scenarios.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
message: The A2A message to include.
|
|
230
|
+
invocation_context: The invocation context.
|
|
231
|
+
event: The ADK event.
|
|
232
|
+
task_id: Optional task ID to use for generated events.
|
|
233
|
+
context_id: Optional Context ID to use for generated events.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A TaskStatusUpdateEvent with RUNNING state.
|
|
238
|
+
"""
|
|
239
|
+
status = TaskStatus(
|
|
240
|
+
state=TaskState.working,
|
|
241
|
+
message=message,
|
|
242
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if any(
|
|
246
|
+
part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
247
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
248
|
+
and part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)) is True
|
|
249
|
+
and part.root.data.get("name") == REQUEST_EUC_FUNCTION_CALL_NAME
|
|
250
|
+
for part in message.parts
|
|
251
|
+
if part.root.metadata
|
|
252
|
+
):
|
|
253
|
+
status.state = TaskState.auth_required
|
|
254
|
+
elif any(
|
|
255
|
+
part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY))
|
|
256
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
257
|
+
and part.root.metadata.get(get_kagent_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY)) is True
|
|
258
|
+
for part in message.parts
|
|
259
|
+
if part.root.metadata
|
|
260
|
+
):
|
|
261
|
+
status.state = TaskState.input_required
|
|
262
|
+
|
|
263
|
+
return TaskStatusUpdateEvent(
|
|
264
|
+
task_id=task_id,
|
|
265
|
+
context_id=context_id,
|
|
266
|
+
status=status,
|
|
267
|
+
metadata=_get_context_metadata(event, invocation_context),
|
|
268
|
+
final=False,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def convert_event_to_a2a_events(
|
|
273
|
+
event: Event,
|
|
274
|
+
invocation_context: InvocationContext,
|
|
275
|
+
task_id: Optional[str] = None,
|
|
276
|
+
context_id: Optional[str] = None,
|
|
277
|
+
) -> List[A2AEvent]:
|
|
278
|
+
"""Converts a GenAI event to a list of A2A events.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
event: The ADK event to convert.
|
|
282
|
+
invocation_context: The invocation context.
|
|
283
|
+
task_id: Optional task ID to use for generated events.
|
|
284
|
+
context_id: Optional Context ID to use for generated events.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A list of A2A events representing the converted ADK event.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ValueError: If required parameters are invalid.
|
|
291
|
+
"""
|
|
292
|
+
if not event:
|
|
293
|
+
raise ValueError("Event cannot be None")
|
|
294
|
+
if not invocation_context:
|
|
295
|
+
raise ValueError("Invocation context cannot be None")
|
|
296
|
+
|
|
297
|
+
a2a_events = []
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
# Handle error scenarios
|
|
301
|
+
if event.error_code:
|
|
302
|
+
error_event = _create_error_status_event(event, invocation_context, task_id, context_id)
|
|
303
|
+
a2a_events.append(error_event)
|
|
304
|
+
|
|
305
|
+
# Handle regular message content
|
|
306
|
+
message = convert_event_to_a2a_message(event, invocation_context)
|
|
307
|
+
if message:
|
|
308
|
+
running_event = _create_status_update_event(message, invocation_context, event, task_id, context_id)
|
|
309
|
+
a2a_events.append(running_event)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error("Failed to convert event to A2A events: %s", e)
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
return a2a_events
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
module containing utilities for conversion betwen A2A Part and Google GenAI Part
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import base64
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from a2a import types as a2a_types
|
|
27
|
+
from google.genai import types as genai_types
|
|
28
|
+
|
|
29
|
+
from kagent.core.a2a import (
|
|
30
|
+
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT,
|
|
31
|
+
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE,
|
|
32
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
|
33
|
+
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
|
34
|
+
A2A_DATA_PART_METADATA_TYPE_KEY,
|
|
35
|
+
get_kagent_metadata_key,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("kagent_adk." + __name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def convert_a2a_part_to_genai_part(
|
|
42
|
+
a2a_part: a2a_types.Part,
|
|
43
|
+
) -> Optional[genai_types.Part]:
|
|
44
|
+
"""Convert an A2A Part to a Google GenAI Part."""
|
|
45
|
+
part = a2a_part.root
|
|
46
|
+
if isinstance(part, a2a_types.TextPart):
|
|
47
|
+
return genai_types.Part(text=part.text)
|
|
48
|
+
|
|
49
|
+
if isinstance(part, a2a_types.FilePart):
|
|
50
|
+
if isinstance(part.file, a2a_types.FileWithUri):
|
|
51
|
+
return genai_types.Part(
|
|
52
|
+
file_data=genai_types.FileData(file_uri=part.file.uri, mime_type=part.file.mime_type)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
elif isinstance(part.file, a2a_types.FileWithBytes):
|
|
56
|
+
return genai_types.Part(
|
|
57
|
+
inline_data=genai_types.Blob(
|
|
58
|
+
data=base64.b64decode(part.file.bytes),
|
|
59
|
+
mime_type=part.file.mime_type,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Cannot convert unsupported file type: %s for A2A part: %s",
|
|
65
|
+
type(part.file),
|
|
66
|
+
a2a_part,
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
if isinstance(part, a2a_types.DataPart):
|
|
71
|
+
# Conver the Data Part to funcall and function reponse.
|
|
72
|
+
# This is mainly for converting human in the loop and auth request and
|
|
73
|
+
# response.
|
|
74
|
+
# TODO once A2A defined how to suervice such information, migrate below
|
|
75
|
+
# logic accordinlgy
|
|
76
|
+
if part.metadata and get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY) in part.metadata:
|
|
77
|
+
if (
|
|
78
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
79
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
80
|
+
):
|
|
81
|
+
return genai_types.Part(function_call=genai_types.FunctionCall.model_validate(part.data, by_alias=True))
|
|
82
|
+
if (
|
|
83
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
84
|
+
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
|
85
|
+
):
|
|
86
|
+
return genai_types.Part(
|
|
87
|
+
function_response=genai_types.FunctionResponse.model_validate(part.data, by_alias=True)
|
|
88
|
+
)
|
|
89
|
+
if (
|
|
90
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
91
|
+
== A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
|
92
|
+
):
|
|
93
|
+
return genai_types.Part(
|
|
94
|
+
code_execution_result=genai_types.CodeExecutionResult.model_validate(part.data, by_alias=True)
|
|
95
|
+
)
|
|
96
|
+
if (
|
|
97
|
+
part.metadata[get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
|
98
|
+
== A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
|
99
|
+
):
|
|
100
|
+
return genai_types.Part(
|
|
101
|
+
executable_code=genai_types.ExecutableCode.model_validate(part.data, by_alias=True)
|
|
102
|
+
)
|
|
103
|
+
return genai_types.Part(text=json.dumps(part.data))
|
|
104
|
+
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Cannot convert unsupported part type: %s for A2A part: %s",
|
|
107
|
+
type(part),
|
|
108
|
+
a2a_part,
|
|
109
|
+
)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def convert_genai_part_to_a2a_part(
|
|
114
|
+
part: genai_types.Part,
|
|
115
|
+
) -> Optional[a2a_types.Part]:
|
|
116
|
+
"""Convert a Google GenAI Part to an A2A Part."""
|
|
117
|
+
|
|
118
|
+
if part.text:
|
|
119
|
+
a2a_part = a2a_types.TextPart(text=part.text)
|
|
120
|
+
if part.thought is not None:
|
|
121
|
+
a2a_part.metadata = {get_kagent_metadata_key("thought"): part.thought}
|
|
122
|
+
return a2a_types.Part(root=a2a_part)
|
|
123
|
+
|
|
124
|
+
if part.file_data:
|
|
125
|
+
return a2a_types.Part(
|
|
126
|
+
root=a2a_types.FilePart(
|
|
127
|
+
file=a2a_types.FileWithUri(
|
|
128
|
+
uri=part.file_data.file_uri,
|
|
129
|
+
mime_type=part.file_data.mime_type,
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if part.inline_data:
|
|
135
|
+
a2a_part = a2a_types.FilePart(
|
|
136
|
+
file=a2a_types.FileWithBytes(
|
|
137
|
+
bytes=base64.b64encode(part.inline_data.data).decode("utf-8"),
|
|
138
|
+
mime_type=part.inline_data.mime_type,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if part.video_metadata:
|
|
143
|
+
a2a_part.metadata = {
|
|
144
|
+
get_kagent_metadata_key("video_metadata"): part.video_metadata.model_dump(
|
|
145
|
+
by_alias=True, exclude_none=True
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return a2a_types.Part(root=a2a_part)
|
|
150
|
+
|
|
151
|
+
# Conver the funcall and function reponse to A2A DataPart.
|
|
152
|
+
# This is mainly for converting human in the loop and auth request and
|
|
153
|
+
# response.
|
|
154
|
+
# TODO once A2A defined how to suervice such information, migrate below
|
|
155
|
+
# logic accordinlgy
|
|
156
|
+
if part.function_call:
|
|
157
|
+
return a2a_types.Part(
|
|
158
|
+
root=a2a_types.DataPart(
|
|
159
|
+
data=part.function_call.model_dump(by_alias=True, exclude_none=True),
|
|
160
|
+
metadata={
|
|
161
|
+
get_kagent_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if part.function_response:
|
|
167
|
+
return a2a_types.Part(
|
|
168
|
+
root=a2a_types.DataPart(
|
|
169
|
+
data=part.function_response.model_dump(by_alias=True, exclude_none=True),
|
|
170
|
+
metadata={
|
|
171
|
+
get_kagent_metadata_key(
|
|
172
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
173
|
+
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if part.code_execution_result:
|
|
179
|
+
return a2a_types.Part(
|
|
180
|
+
root=a2a_types.DataPart(
|
|
181
|
+
data=part.code_execution_result.model_dump(by_alias=True, exclude_none=True),
|
|
182
|
+
metadata={
|
|
183
|
+
get_kagent_metadata_key(
|
|
184
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
185
|
+
): A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if part.executable_code:
|
|
191
|
+
return a2a_types.Part(
|
|
192
|
+
root=a2a_types.DataPart(
|
|
193
|
+
data=part.executable_code.model_dump(by_alias=True, exclude_none=True),
|
|
194
|
+
metadata={
|
|
195
|
+
get_kagent_metadata_key(
|
|
196
|
+
A2A_DATA_PART_METADATA_TYPE_KEY
|
|
197
|
+
): A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
logger.warning(
|
|
203
|
+
"Cannot convert unsupported part for Google GenAI part: %s",
|
|
204
|
+
part,
|
|
205
|
+
)
|
|
206
|
+
return None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from a2a.server.agent_execution import RequestContext
|
|
4
|
+
from google.adk.runners import RunConfig
|
|
5
|
+
from google.genai import types as genai_types
|
|
6
|
+
|
|
7
|
+
from .part_converter import convert_a2a_part_to_genai_part
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_user_id(request: RequestContext) -> str:
|
|
11
|
+
# Get user from call context if available (auth is enabled on a2a server)
|
|
12
|
+
if request.call_context and request.call_context.user and request.call_context.user.user_name:
|
|
13
|
+
return request.call_context.user.user_name
|
|
14
|
+
|
|
15
|
+
# Get user from context id
|
|
16
|
+
return f"A2A_USER_{request.context_id}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def convert_a2a_request_to_adk_run_args(
|
|
20
|
+
request: RequestContext,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
if not request.message:
|
|
23
|
+
raise ValueError("Request message cannot be None")
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"user_id": _get_user_id(request),
|
|
27
|
+
"session_id": request.context_id,
|
|
28
|
+
"new_message": genai_types.Content(
|
|
29
|
+
role="user",
|
|
30
|
+
parts=[convert_a2a_part_to_genai_part(part) for part in request.message.parts],
|
|
31
|
+
),
|
|
32
|
+
"run_config": RunConfig(),
|
|
33
|
+
}
|
|
@@ -4,8 +4,7 @@ from typing import Literal, Self, Union
|
|
|
4
4
|
from google.adk.agents import Agent
|
|
5
5
|
from google.adk.agents.base_agent import BaseAgent
|
|
6
6
|
from google.adk.agents.llm_agent import ToolUnion
|
|
7
|
-
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
|
8
|
-
from google.adk.agents.run_config import RunConfig, StreamingMode
|
|
7
|
+
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH, RemoteA2aAgent
|
|
9
8
|
from google.adk.models.anthropic_llm import Claude as ClaudeLLM
|
|
10
9
|
from google.adk.models.google_llm import Gemini as GeminiLLM
|
|
11
10
|
from google.adk.models.lite_llm import LiteLlm
|
|
@@ -76,7 +75,6 @@ class AgentConfig(BaseModel):
|
|
|
76
75
|
instruction: str
|
|
77
76
|
http_tools: list[HttpMcpServerConfig] | None = None # tools, always MCP
|
|
78
77
|
sse_tools: list[SseMcpServerConfig] | None = None # tools, always MCP
|
|
79
|
-
agents: list[Self] | None = None # agent names
|
|
80
78
|
remote_agents: list[RemoteAgentConfig] | None = None # remote agents
|
|
81
79
|
|
|
82
80
|
def to_agent(self, name: str) -> Agent:
|
|
@@ -89,19 +87,15 @@ class AgentConfig(BaseModel):
|
|
|
89
87
|
if self.sse_tools:
|
|
90
88
|
for sse_tool in self.sse_tools: # add stdio tools
|
|
91
89
|
mcp_toolsets.append(MCPToolset(connection_params=sse_tool.params, tool_filter=sse_tool.tools))
|
|
92
|
-
if self.agents:
|
|
93
|
-
for agent in self.agents: # Add sub agents as tools
|
|
94
|
-
mcp_toolsets.append(AgentTool(agent.to_agent(name)))
|
|
95
|
-
remote_agents: list[BaseAgent] = []
|
|
96
90
|
if self.remote_agents:
|
|
97
91
|
for remote_agent in self.remote_agents: # Add remote agents as tools
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
description=remote_agent.description,
|
|
103
|
-
)
|
|
92
|
+
remote_agent = RemoteA2aAgent(
|
|
93
|
+
name=remote_agent.name,
|
|
94
|
+
agent_card=f"{remote_agent.url}/{AGENT_CARD_WELL_KNOWN_PATH}",
|
|
95
|
+
description=remote_agent.description,
|
|
104
96
|
)
|
|
97
|
+
mcp_toolsets.append(AgentTool(agent=remote_agent, skip_summarization=True))
|
|
98
|
+
|
|
105
99
|
if self.model.type == "openai":
|
|
106
100
|
model = LiteLlm(model=f"openai/{self.model.model}", base_url=self.model.base_url)
|
|
107
101
|
elif self.model.type == "anthropic":
|
|
@@ -124,5 +118,4 @@ class AgentConfig(BaseModel):
|
|
|
124
118
|
description=self.description,
|
|
125
119
|
instruction=self.instruction,
|
|
126
120
|
tools=mcp_toolsets,
|
|
127
|
-
sub_agents=remote_agents,
|
|
128
121
|
)
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from typing import override
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
from a2a.server.tasks import TaskStore
|
|
5
|
-
from a2a.types import Task
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class KAgentTaskStore(TaskStore):
|
|
9
|
-
client: httpx.AsyncClient
|
|
10
|
-
|
|
11
|
-
def __init__(self, client: httpx.AsyncClient):
|
|
12
|
-
self.client = client
|
|
13
|
-
|
|
14
|
-
@override
|
|
15
|
-
async def save(self, task: Task) -> None:
|
|
16
|
-
response = await self.client.post("/api/tasks", json=task.model_dump())
|
|
17
|
-
response.raise_for_status()
|
|
18
|
-
|
|
19
|
-
@override
|
|
20
|
-
async def get(self, task_id: str) -> Task | None:
|
|
21
|
-
response = await self.client.get(f"/api/tasks/{task_id}")
|
|
22
|
-
if response.status_code == 404:
|
|
23
|
-
return None
|
|
24
|
-
response.raise_for_status()
|
|
25
|
-
return Task.model_validate(response.json())
|
|
26
|
-
|
|
27
|
-
@override
|
|
28
|
-
async def delete(self, task_id: str) -> None:
|
|
29
|
-
response = await self.client.delete(f"/api/tasks/{task_id}")
|
|
30
|
-
response.raise_for_status()
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
|
|
7
|
-
import typer
|
|
8
|
-
import uvicorn
|
|
9
|
-
from a2a.types import AgentCard
|
|
10
|
-
from google.adk.cli.utils.agent_loader import AgentLoader
|
|
11
|
-
from opentelemetry import _logs, trace
|
|
12
|
-
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
|
|
13
|
-
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
14
|
-
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
15
|
-
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
|
16
|
-
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
17
|
-
from opentelemetry.sdk._events import EventLoggerProvider
|
|
18
|
-
from opentelemetry.sdk._logs import LoggerProvider
|
|
19
|
-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
20
|
-
from opentelemetry.sdk.resources import Resource
|
|
21
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
22
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
23
|
-
|
|
24
|
-
from . import AgentConfig, KAgentApp
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
app = typer.Typer()
|
|
29
|
-
|
|
30
|
-
kagent_url = os.getenv("KAGENT_URL")
|
|
31
|
-
kagent_name = os.getenv("KAGENT_NAME")
|
|
32
|
-
kagent_namespace = os.getenv("KAGENT_NAMESPACE")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Config:
|
|
36
|
-
_url: str
|
|
37
|
-
_name: str
|
|
38
|
-
_namespace: str
|
|
39
|
-
|
|
40
|
-
def __init__(self):
|
|
41
|
-
if not kagent_url:
|
|
42
|
-
raise ValueError("KAGENT_URL is not set")
|
|
43
|
-
if not kagent_name:
|
|
44
|
-
raise ValueError("KAGENT_NAME is not set")
|
|
45
|
-
if not kagent_namespace:
|
|
46
|
-
raise ValueError("KAGENT_NAMESPACE is not set")
|
|
47
|
-
self._url = kagent_url
|
|
48
|
-
self._name = kagent_name
|
|
49
|
-
self._namespace = kagent_namespace
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def name(self):
|
|
53
|
-
return self._name.replace("-", "_")
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def namespace(self):
|
|
57
|
-
return self._namespace.replace("-", "_")
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def app_name(self):
|
|
61
|
-
return self.namespace + "__NS__" + self.name
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def url(self):
|
|
65
|
-
return self._url
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def configure_tracing():
|
|
69
|
-
tracing_enabled = os.getenv("OTEL_TRACING_ENABLED", "false").lower() == "true"
|
|
70
|
-
logging_enabled = os.getenv("OTEL_LOGGING_ENABLED", "false").lower() == "true"
|
|
71
|
-
|
|
72
|
-
resource = Resource({"service.name": "kagent"})
|
|
73
|
-
|
|
74
|
-
# Configure tracing if enabled
|
|
75
|
-
if tracing_enabled:
|
|
76
|
-
logging.info("Enabling tracing")
|
|
77
|
-
tracer_provider = TracerProvider(resource=resource)
|
|
78
|
-
# Check new env var first, fall back to old one for backward compatibility
|
|
79
|
-
trace_endpoint = os.getenv("OTEL_TRACING_EXPORTER_OTLP_ENDPOINT") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
80
|
-
if trace_endpoint:
|
|
81
|
-
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=trace_endpoint))
|
|
82
|
-
else:
|
|
83
|
-
processor = BatchSpanProcessor(OTLPSpanExporter())
|
|
84
|
-
tracer_provider.add_span_processor(processor)
|
|
85
|
-
trace.set_tracer_provider(tracer_provider)
|
|
86
|
-
HTTPXClientInstrumentor().instrument()
|
|
87
|
-
|
|
88
|
-
# Configure logging if enabled
|
|
89
|
-
if logging_enabled:
|
|
90
|
-
logging.info("Enabling logging for GenAI events")
|
|
91
|
-
logger_provider = LoggerProvider(resource=resource)
|
|
92
|
-
log_endpoint = os.getenv("OTEL_LOGGING_EXPORTER_OTLP_ENDPOINT")
|
|
93
|
-
logging.info(f"Log endpoint configured: {log_endpoint}")
|
|
94
|
-
|
|
95
|
-
# Add OTLP exporter
|
|
96
|
-
if log_endpoint:
|
|
97
|
-
log_processor = BatchLogRecordProcessor(OTLPLogExporter(endpoint=log_endpoint))
|
|
98
|
-
else:
|
|
99
|
-
log_processor = BatchLogRecordProcessor(OTLPLogExporter())
|
|
100
|
-
logger_provider.add_log_record_processor(log_processor)
|
|
101
|
-
|
|
102
|
-
_logs.set_logger_provider(logger_provider)
|
|
103
|
-
logging.info("Log provider configured with OTLP")
|
|
104
|
-
# When logging is enabled, use new event-based approach (input/output as log events in Body)
|
|
105
|
-
logging.info("OpenAI instrumentation configured with event logging capability")
|
|
106
|
-
# Create event logger provider using the configured logger provider
|
|
107
|
-
event_logger_provider = EventLoggerProvider(logger_provider)
|
|
108
|
-
OpenAIInstrumentor(use_legacy_attributes=False).instrument(event_logger_provider=event_logger_provider)
|
|
109
|
-
AnthropicInstrumentor(use_legacy_attributes=False).instrument(event_logger_provider=event_logger_provider)
|
|
110
|
-
else:
|
|
111
|
-
# Use legacy attributes (input/output as GenAI span attributes)
|
|
112
|
-
logging.info("OpenAI instrumentation configured with legacy GenAI span attributes")
|
|
113
|
-
OpenAIInstrumentor().instrument()
|
|
114
|
-
AnthropicInstrumentor().instrument()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@app.command()
|
|
118
|
-
def static(
|
|
119
|
-
host: str = "127.0.0.1",
|
|
120
|
-
port: int = 8080,
|
|
121
|
-
workers: int = 1,
|
|
122
|
-
filepath: str = "/config",
|
|
123
|
-
reload: Annotated[bool, typer.Option("--reload")] = False,
|
|
124
|
-
):
|
|
125
|
-
configure_tracing()
|
|
126
|
-
|
|
127
|
-
app_cfg = Config()
|
|
128
|
-
|
|
129
|
-
with open(os.path.join(filepath, "config.json"), "r") as f:
|
|
130
|
-
config = json.load(f)
|
|
131
|
-
agent_config = AgentConfig.model_validate(config)
|
|
132
|
-
with open(os.path.join(filepath, "agent-card.json"), "r") as f:
|
|
133
|
-
agent_card = json.load(f)
|
|
134
|
-
agent_card = AgentCard.model_validate(agent_card)
|
|
135
|
-
root_agent = agent_config.to_agent(app_cfg.name)
|
|
136
|
-
|
|
137
|
-
kagent_app = KAgentApp(root_agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
138
|
-
|
|
139
|
-
uvicorn.run(
|
|
140
|
-
kagent_app.build,
|
|
141
|
-
host=host,
|
|
142
|
-
port=port,
|
|
143
|
-
workers=workers,
|
|
144
|
-
reload=reload,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
@app.command()
|
|
149
|
-
def run(
|
|
150
|
-
name: Annotated[str, typer.Argument(help="The name of the agent to run")],
|
|
151
|
-
working_dir: str = ".",
|
|
152
|
-
host: str = "127.0.0.1",
|
|
153
|
-
port: int = 8080,
|
|
154
|
-
workers: int = 1,
|
|
155
|
-
):
|
|
156
|
-
configure_tracing()
|
|
157
|
-
app_cfg = Config()
|
|
158
|
-
|
|
159
|
-
agent_loader = AgentLoader(agents_dir=working_dir)
|
|
160
|
-
root_agent = agent_loader.load_agent(name)
|
|
161
|
-
|
|
162
|
-
with open(os.path.join(working_dir, name, "agent-card.json"), "r") as f:
|
|
163
|
-
agent_card = json.load(f)
|
|
164
|
-
agent_card = AgentCard.model_validate(agent_card)
|
|
165
|
-
kagent_app = KAgentApp(root_agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
166
|
-
uvicorn.run(
|
|
167
|
-
kagent_app.build,
|
|
168
|
-
host=host,
|
|
169
|
-
port=port,
|
|
170
|
-
workers=workers,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
async def test_agent(agent_config: AgentConfig, agent_card: AgentCard, task: str):
|
|
175
|
-
app_cfg = Config()
|
|
176
|
-
agent = agent_config.to_agent(app_cfg.name)
|
|
177
|
-
app = KAgentApp(agent, agent_card, app_cfg.url, app_cfg.app_name)
|
|
178
|
-
await app.test(task)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
@app.command()
|
|
182
|
-
def test(
|
|
183
|
-
task: Annotated[str, typer.Option("--task", help="The task to test the agent with")],
|
|
184
|
-
filepath: Annotated[str, typer.Option("--filepath", help="The path to the agent config file")],
|
|
185
|
-
):
|
|
186
|
-
with open(filepath, "r") as f:
|
|
187
|
-
content = f.read()
|
|
188
|
-
config = json.loads(content)
|
|
189
|
-
|
|
190
|
-
with open(os.path.join(filepath, "agent-card.json"), "r") as f:
|
|
191
|
-
agent_card = json.load(f)
|
|
192
|
-
agent_card = AgentCard.model_validate(agent_card)
|
|
193
|
-
agent_config = AgentConfig.model_validate(config)
|
|
194
|
-
asyncio.run(test_agent(agent_config, agent_card, task))
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def run_cli():
|
|
198
|
-
logging.basicConfig(level=logging.INFO)
|
|
199
|
-
logging.info("Starting KAgent")
|
|
200
|
-
app()
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if __name__ == "__main__":
|
|
204
|
-
run_cli()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|