langgraph-agent-common 2.0.0__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.
- langgraph_a2a_api/__init__.py +0 -0
- langgraph_a2a_api/adaptor/__init__.py +0 -0
- langgraph_a2a_api/adaptor/agent_executor.py +421 -0
- langgraph_a2a_api/adaptor/conversion.py +167 -0
- langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +34 -0
- langgraph_a2a_api/fastapi_application.py +152 -0
- langgraph_a2a_api/langgraph/__init__.py +0 -0
- langgraph_a2a_api/langgraph/checkpointer.py +102 -0
- langgraph_a2a_api/langgraph/graph.py +49 -0
- langgraph_a2a_api/langgraph/store.py +64 -0
- langgraph_a2a_api/langgraph/validator.py +42 -0
- langgraph_a2a_api/resources/openapi_schema.json +277 -0
- langgraph_a2a_api/resources/static/css/style.css +480 -0
- langgraph_a2a_api/resources/static/index.html +107 -0
- langgraph_a2a_api/resources/static/js/app.js +896 -0
- langgraph_a2a_api/server.py +149 -0
- langgraph_a2a_api/tasks/__init__.py +0 -0
- langgraph_a2a_api/tasks/push_notification_config_store.py +24 -0
- langgraph_a2a_api/tasks/task_store.py +66 -0
- langgraph_a2a_api/utils/__init__.py +0 -0
- langgraph_a2a_api/utils/lock.py +31 -0
- langgraph_a2a_api/utils/types.py +3 -0
- langgraph_agent_common-2.0.0.dist-info/METADATA +67 -0
- langgraph_agent_common-2.0.0.dist-info/RECORD +31 -0
- langgraph_agent_common-2.0.0.dist-info/WHEEL +5 -0
- langgraph_agent_common-2.0.0.dist-info/top_level.txt +2 -0
- langgraph_aux/__init__.py +0 -0
- langgraph_aux/callbacks/__init__.py +11 -0
- langgraph_aux/callbacks/talos_callback_handler.py +340 -0
- langgraph_aux/utils/__init__.py +0 -0
- langgraph_aux/utils/stream_output.py +13 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.openapi.utils import get_openapi
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
from fastapi.staticfiles import StaticFiles
|
|
9
|
+
from starlette.requests import Request
|
|
10
|
+
from starlette.responses import Response
|
|
11
|
+
from starlette.routing import BaseRoute, Mount
|
|
12
|
+
|
|
13
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
14
|
+
from a2a.server.routes.agent_card_routes import create_agent_card_routes
|
|
15
|
+
from a2a.server.routes.jsonrpc_routes import create_jsonrpc_routes
|
|
16
|
+
from a2a.server.routes.rest_routes import create_rest_routes
|
|
17
|
+
from a2a.types import AgentCard
|
|
18
|
+
from a2a.utils import constants
|
|
19
|
+
|
|
20
|
+
_OPENAPI_SCHEMA_PATH = Path(__file__).parent / "resources" / "openapi_schema.json"
|
|
21
|
+
_STATIC_PATH = Path(__file__).parent / "resources" / "static"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_openapi_schema() -> dict:
|
|
25
|
+
with open(_OPENAPI_SCHEMA_PATH) as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _create_a2a_routes(agent_card: AgentCard, request_handler: DefaultRequestHandler) -> list[BaseRoute]:
|
|
30
|
+
return [
|
|
31
|
+
*create_agent_card_routes(agent_card),
|
|
32
|
+
*create_jsonrpc_routes(request_handler, rpc_url='/'),
|
|
33
|
+
*create_rest_routes(request_handler),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _schema_placeholder() -> Response:
|
|
38
|
+
return Response(status_code=501)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _derive_route_tag(path: str) -> str:
|
|
42
|
+
if path == '/':
|
|
43
|
+
return 'jsonrpc'
|
|
44
|
+
if path == '/.well-known/agent-card.json':
|
|
45
|
+
return 'agent-card'
|
|
46
|
+
return 'rest'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _derive_route_summary(route: BaseRoute) -> str:
|
|
50
|
+
endpoint = getattr(route, 'endpoint', None)
|
|
51
|
+
path = getattr(route, 'path', 'A2A route')
|
|
52
|
+
if endpoint is None:
|
|
53
|
+
return f'Handle {path}'
|
|
54
|
+
doc = getattr(endpoint, '__doc__', None)
|
|
55
|
+
if doc:
|
|
56
|
+
return doc.strip().splitlines()[0]
|
|
57
|
+
name = getattr(route, 'name', None) or getattr(endpoint, '__name__', None)
|
|
58
|
+
if name:
|
|
59
|
+
return name.replace('_', ' ').strip().capitalize()
|
|
60
|
+
return f'Handle {path}'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _register_routes(app: FastAPI, routes: list[BaseRoute]):
|
|
64
|
+
for route in routes:
|
|
65
|
+
app.router.routes.append(route)
|
|
66
|
+
if isinstance(route, Mount):
|
|
67
|
+
continue
|
|
68
|
+
methods = sorted(method for method in (getattr(route, 'methods', None) or []) if method != 'HEAD')
|
|
69
|
+
if not methods:
|
|
70
|
+
continue
|
|
71
|
+
app.add_api_route(
|
|
72
|
+
path=route.path,
|
|
73
|
+
endpoint=_schema_placeholder,
|
|
74
|
+
methods=methods,
|
|
75
|
+
include_in_schema=True,
|
|
76
|
+
tags=[_derive_route_tag(route.path)],
|
|
77
|
+
summary=_derive_route_summary(route),
|
|
78
|
+
name=f"openapi:{','.join(methods)}:{route.path}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _add_common_routes(app: FastAPI):
|
|
83
|
+
if not _STATIC_PATH.exists():
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
app.mount('/static', StaticFiles(directory=str(_STATIC_PATH)), name='static')
|
|
87
|
+
|
|
88
|
+
@app.get('/chat')
|
|
89
|
+
async def chat_ui():
|
|
90
|
+
return FileResponse(str(_STATIC_PATH / 'index.html'))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_fastapi_application(agent_card: AgentCard, request_handler: DefaultRequestHandler, lifespan: Any, root_path: str = "") -> FastAPI:
|
|
94
|
+
app = FastAPI(
|
|
95
|
+
lifespan=lifespan,
|
|
96
|
+
title=agent_card.name or 'A2A Agent API',
|
|
97
|
+
description=agent_card.description or 'A2A Protocol API with JSON-RPC and REST endpoints',
|
|
98
|
+
version=agent_card.version or '1.0.0',
|
|
99
|
+
docs_url='/docs',
|
|
100
|
+
redoc_url='/redoc',
|
|
101
|
+
openapi_url='/openapi.json',
|
|
102
|
+
root_path=root_path,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
routes = _create_a2a_routes(agent_card, request_handler)
|
|
106
|
+
|
|
107
|
+
@app.middleware('http')
|
|
108
|
+
async def ensure_a2a_version_header(request: Request, call_next):
|
|
109
|
+
if constants.VERSION_HEADER.encode() not in request.headers.raw:
|
|
110
|
+
request.scope['headers'] = [
|
|
111
|
+
*request.scope['headers'],
|
|
112
|
+
(constants.VERSION_HEADER.lower().encode(), constants.PROTOCOL_VERSION_1_0.encode()),
|
|
113
|
+
]
|
|
114
|
+
return await call_next(request)
|
|
115
|
+
|
|
116
|
+
_add_common_routes(app)
|
|
117
|
+
_register_routes(app, routes)
|
|
118
|
+
_customize_openapi(app)
|
|
119
|
+
return app
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _customize_openapi(app: FastAPI):
|
|
123
|
+
external_schema = _load_openapi_schema()
|
|
124
|
+
|
|
125
|
+
def openapi_schema():
|
|
126
|
+
if app.openapi_schema:
|
|
127
|
+
return app.openapi_schema
|
|
128
|
+
|
|
129
|
+
schema = get_openapi(
|
|
130
|
+
title=app.title,
|
|
131
|
+
version=app.version,
|
|
132
|
+
openapi_version=app.openapi_version,
|
|
133
|
+
description=app.description,
|
|
134
|
+
routes=app.routes,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
external_schemas = external_schema.get('components', {}).get('schemas', {})
|
|
138
|
+
if external_schemas:
|
|
139
|
+
schema.setdefault('components', {}).setdefault('schemas', {}).update(external_schemas)
|
|
140
|
+
|
|
141
|
+
external_paths = external_schema.get('paths', {})
|
|
142
|
+
app_paths = schema.get('paths', {})
|
|
143
|
+
for path, methods in external_paths.items():
|
|
144
|
+
if path in app_paths:
|
|
145
|
+
for method, config in methods.items():
|
|
146
|
+
if method in app_paths[path]:
|
|
147
|
+
app_paths[path][method].update(config)
|
|
148
|
+
|
|
149
|
+
app.openapi_schema = schema
|
|
150
|
+
return app.openapi_schema
|
|
151
|
+
|
|
152
|
+
app.openapi = openapi_schema
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
|
2
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
3
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from psycopg.rows import DictRow
|
|
9
|
+
from psycopg import AsyncCursor
|
|
10
|
+
from psycopg_pool import AsyncConnectionPool
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import os
|
|
14
|
+
import textwrap
|
|
15
|
+
|
|
16
|
+
class AsyncPostgresSaverWrapper(AsyncPostgresSaver):
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.checkpoint_table_prefix = None
|
|
20
|
+
self.GET_EXPIRED_THREADS_SQL = textwrap.dedent("""
|
|
21
|
+
SELECT thread_id
|
|
22
|
+
FROM checkpoints
|
|
23
|
+
GROUP BY thread_id
|
|
24
|
+
HAVING MAX((checkpoint ->> 'ts')::timestamptz) < (NOW() - (%s || ' MINUTES')::INTERVAL);
|
|
25
|
+
""")
|
|
26
|
+
|
|
27
|
+
def set_checkpoint_table_prefix(self, checkpoint_table_prefix: str):
|
|
28
|
+
self.checkpoint_table_prefix = checkpoint_table_prefix
|
|
29
|
+
|
|
30
|
+
@asynccontextmanager
|
|
31
|
+
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor[DictRow]]:
|
|
32
|
+
async with super()._cursor(pipeline = pipeline) as cur:
|
|
33
|
+
yield AsyncCursorWrapper(cur, self.checkpoint_table_prefix)
|
|
34
|
+
|
|
35
|
+
async def sweep_threads(self, ttl: int) -> int:
|
|
36
|
+
async with self._cursor() as cur:
|
|
37
|
+
await cur.execute(self.GET_EXPIRED_THREADS_SQL, (ttl,), binary=True)
|
|
38
|
+
values = await cur.fetchmany(1024)
|
|
39
|
+
for value in values:
|
|
40
|
+
await self.adelete_thread(value["thread_id"])
|
|
41
|
+
return len(values)
|
|
42
|
+
|
|
43
|
+
class AsyncCursorWrapper():
|
|
44
|
+
def __init__(self, cursor: AsyncCursor, checkpoint_table_prefix: str):
|
|
45
|
+
self._cursor = cursor
|
|
46
|
+
self.checkpoint_table_prefix = checkpoint_table_prefix
|
|
47
|
+
|
|
48
|
+
def _add_checkpoint_table_prefix(self, query: str) -> str:
|
|
49
|
+
checkpoint_migrations_regex = r"(?<=[ ])checkpoint_migrations(?=[_ .(]|\n)"
|
|
50
|
+
checkpoints_regex = r"(?<=[ ])checkpoints(?=[_ .(]|\n)"
|
|
51
|
+
checkpoint_blobs_regex = r"(?<=[ ])checkpoint_blobs(?=[_ .(]|\n)"
|
|
52
|
+
checkpoint_writes_regex = r"(?<=[ ])checkpoint_writes(?=[_ .(]|\n)"
|
|
53
|
+
checkpoint_index_regex = r"(?<=\W)CREATE INDEX CONCURRENTLY(?=[ ])"
|
|
54
|
+
|
|
55
|
+
checkpoint_migrations_sub = re.sub(checkpoint_migrations_regex, f"{self.checkpoint_table_prefix}__checkpoint_migrations", query)
|
|
56
|
+
checkpoints_sub = re.sub(checkpoints_regex, f"{self.checkpoint_table_prefix}__checkpoints", checkpoint_migrations_sub)
|
|
57
|
+
checkpoint_blobs_sub = re.sub(checkpoint_blobs_regex, f"{self.checkpoint_table_prefix}__checkpoint_blobs", checkpoints_sub)
|
|
58
|
+
checkpoint_writes_sub = re.sub(checkpoint_writes_regex, f"{self.checkpoint_table_prefix}__checkpoint_writes", checkpoint_blobs_sub)
|
|
59
|
+
checkpoint_index_sub = re.sub(checkpoint_index_regex, "CREATE INDEX", checkpoint_writes_sub)
|
|
60
|
+
return checkpoint_index_sub
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def execute(self, *args, **kwargs):
|
|
64
|
+
modified_args = (self._add_checkpoint_table_prefix(args[0]),) + args[1:]
|
|
65
|
+
return self._cursor.execute(*modified_args, **kwargs)
|
|
66
|
+
|
|
67
|
+
def executemany(self, *args, **kwargs):
|
|
68
|
+
modified_args = (self._add_checkpoint_table_prefix(args[0]),) + args[1:]
|
|
69
|
+
return self._cursor.executemany(*modified_args, **kwargs)
|
|
70
|
+
|
|
71
|
+
def __getattr__(self, name):
|
|
72
|
+
return getattr(self._cursor, name)
|
|
73
|
+
|
|
74
|
+
@asynccontextmanager
|
|
75
|
+
async def get_async_postgres_checkpointer(checkpoint_table_prefix: str) -> AsyncIterator[AsyncPostgresSaver]:
|
|
76
|
+
postgres_uri = os.environ["POSTGRES_URI"]
|
|
77
|
+
async with AsyncConnectionPool(postgres_uri, min_size=1, max_size=8, max_idle=600, timeout=30) as pool:
|
|
78
|
+
checkpointer = AsyncPostgresSaverWrapper(conn=pool)
|
|
79
|
+
checkpointer.set_checkpoint_table_prefix(checkpoint_table_prefix)
|
|
80
|
+
await checkpointer.setup()
|
|
81
|
+
yield checkpointer
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class InMemorySaverWrapper(InMemorySaver):
|
|
85
|
+
async def sweep_threads(self, ttl: int) -> int:
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@asynccontextmanager
|
|
90
|
+
async def get_inmemory_checkpointer(checkpoint_table_prefix: str) -> AsyncIterator[BaseCheckpointSaver]:
|
|
91
|
+
yield InMemorySaverWrapper()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@asynccontextmanager
|
|
95
|
+
async def get_checkpointer(checkpoint_table_prefix: str) -> AsyncIterator[BaseCheckpointSaver]:
|
|
96
|
+
if os.getenv("POSTGRES_URI"):
|
|
97
|
+
async with get_async_postgres_checkpointer(checkpoint_table_prefix) as cp:
|
|
98
|
+
yield cp
|
|
99
|
+
else:
|
|
100
|
+
async with get_inmemory_checkpointer(checkpoint_table_prefix) as cp:
|
|
101
|
+
yield cp
|
|
102
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import importlib
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
from random import choice
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
import inflection
|
|
9
|
+
|
|
10
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
11
|
+
from langgraph.types import Checkpointer
|
|
12
|
+
|
|
13
|
+
from langgraph.graph.message import MessagesState
|
|
14
|
+
from a2a.types import AgentSkill
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GraphSpec(NamedTuple):
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
version: str
|
|
21
|
+
skills: list[AgentSkill]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_graph() -> (CompiledStateGraph, GraphSpec):
|
|
25
|
+
path_and_var = os.environ["GRAPH_PATH_AND_VAR"]
|
|
26
|
+
(path, var) = path_and_var.rsplit(":", maxsplit = 1)
|
|
27
|
+
module_name = "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(24))
|
|
28
|
+
module_spec = importlib.util.spec_from_file_location(module_name, path)
|
|
29
|
+
if module_spec is None:
|
|
30
|
+
raise ValueError(f"Could not find python file: {path} for graph: {var}")
|
|
31
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
32
|
+
sys.modules[module_name] = module
|
|
33
|
+
module_spec.loader.exec_module(module)
|
|
34
|
+
|
|
35
|
+
graph = module.__dict__[var]
|
|
36
|
+
if not graph.get_name():
|
|
37
|
+
raise Exception(f"Langgraph graph must include graph name")
|
|
38
|
+
if not issubclass(type(graph.InputType), type(MessagesState)):
|
|
39
|
+
raise Exception(f"Langgraph state must be a subclass of {MessagesState}.")
|
|
40
|
+
description = graph.config.get("metadata", {}).get("description", "")
|
|
41
|
+
version = graph.config.get("metadata", {}).get("version", "1.0.0")
|
|
42
|
+
graph_spec = GraphSpec(
|
|
43
|
+
name = inflection.underscore(graph.get_name()),
|
|
44
|
+
description = description,
|
|
45
|
+
version = version,
|
|
46
|
+
skills = graph.config.get("metadata", {}).get("skills", []),
|
|
47
|
+
)
|
|
48
|
+
return graph, graph_spec
|
|
49
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from psycopg.rows import dict_row
|
|
7
|
+
from psycopg_pool import AsyncConnectionPool
|
|
8
|
+
|
|
9
|
+
from langgraph.store.base import BaseStore
|
|
10
|
+
from langgraph.store.postgres.aio import AsyncPostgresStore
|
|
11
|
+
from langgraph.store.memory import InMemoryStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncPostgresStoreWrapper(AsyncPostgresStore):
|
|
15
|
+
async def sweep(self, ttl: int) -> int:
|
|
16
|
+
async with self._cursor() as cur:
|
|
17
|
+
await cur.execute(
|
|
18
|
+
"""
|
|
19
|
+
DELETE FROM store
|
|
20
|
+
WHERE updated_at < (NOW() - (%s || ' MINUTES')::INTERVAL)
|
|
21
|
+
""",
|
|
22
|
+
(ttl,),
|
|
23
|
+
)
|
|
24
|
+
return cur.rowcount
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@asynccontextmanager
|
|
28
|
+
async def get_async_postgres_store() -> AsyncIterator[BaseStore]:
|
|
29
|
+
postgres_uri = os.environ["POSTGRES_URI"]
|
|
30
|
+
async with AsyncConnectionPool(
|
|
31
|
+
postgres_uri,
|
|
32
|
+
min_size = 1,
|
|
33
|
+
max_size = 8,
|
|
34
|
+
max_idle = 600,
|
|
35
|
+
timeout = 30,
|
|
36
|
+
kwargs = {
|
|
37
|
+
"autocommit": True,
|
|
38
|
+
"prepare_threshold": 0,
|
|
39
|
+
"row_factory": dict_row,
|
|
40
|
+
},
|
|
41
|
+
) as pool:
|
|
42
|
+
store = AsyncPostgresStoreWrapper(conn = pool)
|
|
43
|
+
await store.setup()
|
|
44
|
+
yield store
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class InMemoryStoreWrapper(InMemoryStore):
|
|
48
|
+
async def sweep(self, ttl: int) -> int:
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@asynccontextmanager
|
|
53
|
+
async def get_inmemory_store() -> AsyncIterator[BaseStore]:
|
|
54
|
+
yield InMemoryStoreWrapper()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@asynccontextmanager
|
|
58
|
+
async def get_store() -> AsyncIterator[BaseStore]:
|
|
59
|
+
if os.getenv("POSTGRES_URI"):
|
|
60
|
+
async with get_async_postgres_store() as s:
|
|
61
|
+
yield s
|
|
62
|
+
else:
|
|
63
|
+
async with get_inmemory_store() as s:
|
|
64
|
+
yield s
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, ValidationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HitlAction(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
args: dict[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApproveDecision(BaseModel):
|
|
12
|
+
type: Literal["approve"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EditDecision(BaseModel):
|
|
16
|
+
type: Literal["edit"]
|
|
17
|
+
edited_action: HitlAction
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RejectDecision(BaseModel):
|
|
21
|
+
type: Literal["reject"]
|
|
22
|
+
message: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RespondDecision(BaseModel):
|
|
26
|
+
type: Literal["respond"]
|
|
27
|
+
message: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HITLResponseModel(BaseModel):
|
|
31
|
+
model_config = ConfigDict(extra="forbid")
|
|
32
|
+
|
|
33
|
+
hitl_id: str
|
|
34
|
+
decision: ApproveDecision | EditDecision | RejectDecision | RespondDecision
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_HITLResponse(payload: Any) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
HITLResponseModel.model_validate(payload)
|
|
40
|
+
except ValidationError:
|
|
41
|
+
return False
|
|
42
|
+
return True
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
{
|
|
2
|
+
"components": {
|
|
3
|
+
"schemas": {
|
|
4
|
+
"Part": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"text": { "type": "string", "description": "Text content" },
|
|
8
|
+
"raw": { "type": "string", "format": "byte", "description": "Base64 encoded raw bytes" },
|
|
9
|
+
"url": { "type": "string", "format": "uri", "description": "URL to external content" },
|
|
10
|
+
"data": { "description": "Structured data (JSON object/value)" },
|
|
11
|
+
"metadata": { "type": "object", "description": "Optional metadata" },
|
|
12
|
+
"filename": { "type": "string", "description": "File name" },
|
|
13
|
+
"mediaType": { "type": "string", "description": "MIME type" }
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"Message": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["messageId", "role", "parts"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"messageId": { "type": "string", "description": "Unique message ID (UUID)" },
|
|
21
|
+
"contextId": { "type": "string", "description": "Context ID for grouping related interactions" },
|
|
22
|
+
"taskId": { "type": "string", "description": "Task ID this message belongs to" },
|
|
23
|
+
"role": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"enum": ["ROLE_USER", "ROLE_AGENT"],
|
|
26
|
+
"description": "Message sender role"
|
|
27
|
+
},
|
|
28
|
+
"parts": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"items": { "$ref": "#/components/schemas/Part" },
|
|
31
|
+
"description": "Content parts of the message"
|
|
32
|
+
},
|
|
33
|
+
"metadata": { "type": "object", "description": "Optional metadata for extensions" },
|
|
34
|
+
"extensions": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string" },
|
|
37
|
+
"description": "Extension URIs relevant to this message"
|
|
38
|
+
},
|
|
39
|
+
"referenceTaskIds": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "type": "string" },
|
|
42
|
+
"description": "Related task IDs for context"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"SendMessageConfiguration": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"acceptedOutputModes": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"items": { "type": "string" },
|
|
52
|
+
"description": "Accepted output MIME types"
|
|
53
|
+
},
|
|
54
|
+
"taskPushNotificationConfig": { "$ref": "#/components/schemas/TaskPushNotificationConfig" },
|
|
55
|
+
"historyLength": { "type": "integer", "description": "Number of recent history messages to return" },
|
|
56
|
+
"returnImmediately": { "type": "boolean", "description": "Return immediately without waiting" }
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"SendMessageRequest": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["message"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"tenant": { "type": "string", "description": "Tenant identifier" },
|
|
64
|
+
"message": { "$ref": "#/components/schemas/Message" },
|
|
65
|
+
"configuration": { "$ref": "#/components/schemas/SendMessageConfiguration" },
|
|
66
|
+
"metadata": { "type": "object", "description": "Optional metadata" }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"TaskStatus": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"required": ["state"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"state": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"enum": [
|
|
76
|
+
"TASK_STATE_UNSPECIFIED",
|
|
77
|
+
"TASK_STATE_SUBMITTED",
|
|
78
|
+
"TASK_STATE_WORKING",
|
|
79
|
+
"TASK_STATE_COMPLETED",
|
|
80
|
+
"TASK_STATE_FAILED",
|
|
81
|
+
"TASK_STATE_CANCELED",
|
|
82
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
83
|
+
"TASK_STATE_REJECTED",
|
|
84
|
+
"TASK_STATE_AUTH_REQUIRED"
|
|
85
|
+
],
|
|
86
|
+
"description": "Current task state"
|
|
87
|
+
},
|
|
88
|
+
"message": { "$ref": "#/components/schemas/Message" },
|
|
89
|
+
"timestamp": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp" }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"Artifact": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": ["artifactId", "parts"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"artifactId": { "type": "string", "description": "Unique artifact ID" },
|
|
97
|
+
"name": { "type": "string", "description": "Artifact name" },
|
|
98
|
+
"description": { "type": "string", "description": "Artifact description" },
|
|
99
|
+
"parts": {
|
|
100
|
+
"type": "array",
|
|
101
|
+
"items": { "$ref": "#/components/schemas/Part" },
|
|
102
|
+
"description": "Content parts"
|
|
103
|
+
},
|
|
104
|
+
"metadata": { "type": "object", "description": "Optional metadata" },
|
|
105
|
+
"extensions": {
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": { "type": "string" },
|
|
108
|
+
"description": "Extension URIs"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"Task": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"required": ["id", "contextId", "status"],
|
|
115
|
+
"properties": {
|
|
116
|
+
"id": { "type": "string", "description": "Unique task ID" },
|
|
117
|
+
"contextId": { "type": "string", "description": "Context ID for this task" },
|
|
118
|
+
"status": { "$ref": "#/components/schemas/TaskStatus" },
|
|
119
|
+
"artifacts": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"items": { "$ref": "#/components/schemas/Artifact" },
|
|
122
|
+
"description": "Generated artifacts"
|
|
123
|
+
},
|
|
124
|
+
"history": {
|
|
125
|
+
"type": "array",
|
|
126
|
+
"items": { "$ref": "#/components/schemas/Message" },
|
|
127
|
+
"description": "Conversation history"
|
|
128
|
+
},
|
|
129
|
+
"metadata": { "type": "object", "description": "Optional metadata" }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"SendMessageResponse": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"task": { "$ref": "#/components/schemas/Task" },
|
|
136
|
+
"message": { "$ref": "#/components/schemas/Message" }
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"TaskPushNotificationConfig": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"required": ["id", "taskId", "url"],
|
|
142
|
+
"properties": {
|
|
143
|
+
"tenant": { "type": "string" },
|
|
144
|
+
"id": { "type": "string", "description": "Config ID" },
|
|
145
|
+
"taskId": { "type": "string", "description": "Task ID" },
|
|
146
|
+
"url": { "type": "string", "format": "uri", "description": "Callback URL" },
|
|
147
|
+
"token": { "type": "string", "description": "Auth token" },
|
|
148
|
+
"authentication": { "$ref": "#/components/schemas/AuthenticationInfo" }
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"AuthenticationInfo": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"scheme": { "type": "string", "description": "Auth scheme" },
|
|
155
|
+
"credentials": { "type": "string", "description": "Auth credentials" }
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"JSONRPCRequest": {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"required": ["jsonrpc", "method"],
|
|
161
|
+
"properties": {
|
|
162
|
+
"jsonrpc": { "type": "string", "default": "2.0" },
|
|
163
|
+
"id": { "type": "string", "description": "Request ID" },
|
|
164
|
+
"method": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"enum": [
|
|
167
|
+
"SendMessage",
|
|
168
|
+
"SendStreamingMessage",
|
|
169
|
+
"GetTask",
|
|
170
|
+
"ListTasks",
|
|
171
|
+
"CancelTask",
|
|
172
|
+
"CreateTaskPushNotificationConfig",
|
|
173
|
+
"GetTaskPushNotificationConfig",
|
|
174
|
+
"ListTaskPushNotificationConfigs",
|
|
175
|
+
"DeleteTaskPushNotificationConfig",
|
|
176
|
+
"SubscribeToTask",
|
|
177
|
+
"GetExtendedAgentCard"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
"params": { "type": "object" }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"paths": {
|
|
186
|
+
"/message:send": {
|
|
187
|
+
"post": {
|
|
188
|
+
"requestBody": {
|
|
189
|
+
"required": true,
|
|
190
|
+
"content": {
|
|
191
|
+
"application/json": {
|
|
192
|
+
"schema": { "$ref": "#/components/schemas/SendMessageRequest" }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"responses": {
|
|
197
|
+
"200": {
|
|
198
|
+
"description": "Successful response",
|
|
199
|
+
"content": {
|
|
200
|
+
"application/json": {
|
|
201
|
+
"schema": { "$ref": "#/components/schemas/SendMessageResponse" }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"/message:stream": {
|
|
209
|
+
"post": {
|
|
210
|
+
"requestBody": {
|
|
211
|
+
"required": true,
|
|
212
|
+
"content": {
|
|
213
|
+
"application/json": {
|
|
214
|
+
"schema": { "$ref": "#/components/schemas/SendMessageRequest" }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"responses": {
|
|
219
|
+
"200": {
|
|
220
|
+
"description": "SSE stream of events",
|
|
221
|
+
"content": {
|
|
222
|
+
"text/event-stream": {
|
|
223
|
+
"schema": { "type": "string" }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"/tasks/{id}": {
|
|
231
|
+
"get": {
|
|
232
|
+
"parameters": [
|
|
233
|
+
{
|
|
234
|
+
"name": "id",
|
|
235
|
+
"in": "path",
|
|
236
|
+
"required": true,
|
|
237
|
+
"schema": { "type": "string" },
|
|
238
|
+
"description": "Task ID"
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"responses": {
|
|
242
|
+
"200": {
|
|
243
|
+
"description": "Task details",
|
|
244
|
+
"content": {
|
|
245
|
+
"application/json": {
|
|
246
|
+
"schema": { "$ref": "#/components/schemas/Task" }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
"/tasks/{id}:cancel": {
|
|
254
|
+
"post": {
|
|
255
|
+
"parameters": [
|
|
256
|
+
{
|
|
257
|
+
"name": "id",
|
|
258
|
+
"in": "path",
|
|
259
|
+
"required": true,
|
|
260
|
+
"schema": { "type": "string" },
|
|
261
|
+
"description": "Task ID to cancel"
|
|
262
|
+
}
|
|
263
|
+
],
|
|
264
|
+
"responses": {
|
|
265
|
+
"200": {
|
|
266
|
+
"description": "Canceled task",
|
|
267
|
+
"content": {
|
|
268
|
+
"application/json": {
|
|
269
|
+
"schema": { "$ref": "#/components/schemas/Task" }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|