kagent-adk 0.6.7__py3-none-any.whl → 0.6.9__py3-none-any.whl

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.

@@ -1,7 +1,7 @@
1
1
  import importlib.metadata
2
2
 
3
- from .a2a import KAgentApp
4
- from .models import AgentConfig
3
+ from ._a2a import KAgentApp
4
+ from .types import AgentConfig
5
5
 
6
6
  __version__ = importlib.metadata.version("kagent_adk")
7
7
 
@@ -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 sys
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.auth.user import User
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.server.tasks import TaskStore
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 = A2AStarletteApplication(
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,15 +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
28
25
  from opentelemetry import trace
29
26
  from pydantic import BaseModel
30
27
  from typing_extensions import override
31
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
+
32
34
  logger = logging.getLogger("google_adk." + __name__)
33
35
 
34
36
 
@@ -134,7 +136,7 @@ class A2aAgentExecutor(AgentExecutor):
134
136
  message=Message(
135
137
  message_id=str(uuid.uuid4()),
136
138
  role=Role.agent,
137
- parts=[TextPart(text=str(e))],
139
+ parts=[Part(TextPart(text=str(e)))],
138
140
  ),
139
141
  ),
140
142
  context_id=context.context_id,
@@ -193,9 +195,9 @@ class A2aAgentExecutor(AgentExecutor):
193
195
  context_id=context.context_id,
194
196
  final=False,
195
197
  metadata={
196
- _get_adk_metadata_key("app_name"): runner.app_name,
197
- _get_adk_metadata_key("user_id"): run_args["user_id"],
198
- _get_adk_metadata_key("session_id"): run_args["session_id"],
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"],
199
201
  },
200
202
  )
201
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}
kagent/adk/cli.py ADDED
@@ -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