deepagents-serve 0.1.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.
- deepagents_serve/__init__.py +5 -0
- deepagents_serve/adapter/__init__.py +1 -0
- deepagents_serve/adapter/vercel.py +81 -0
- deepagents_serve/adapter/vercel_protocol.py +193 -0
- deepagents_serve/app.py +33 -0
- deepagents_serve/events.py +53 -0
- deepagents_serve/normaliser.py +54 -0
- deepagents_serve/py.typed +0 -0
- deepagents_serve/route/__init__.py +1 -0
- deepagents_serve/route/health.py +24 -0
- deepagents_serve/route/vercel.py +49 -0
- deepagents_serve-0.1.0.dist-info/METADATA +160 -0
- deepagents_serve-0.1.0.dist-info/RECORD +15 -0
- deepagents_serve-0.1.0.dist-info/WHEEL +5 -0
- deepagents_serve-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters that encode :data:`~deepagents_serve.events.AgentEvent` values into protocol-specific SSE streams."""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Encode :data:`~deepagents_serve.events.AgentEvent` values as Vercel AI Data Protocol SSE frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from deepagents_serve import events
|
|
10
|
+
from deepagents_serve.adapter import vercel_protocol as protocol
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
|
|
15
|
+
from deepagents_serve.events import AgentEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VercelAIProtocolAdapter:
|
|
19
|
+
"""Stateful adapter that converts :class:`~deepagents_serve.events.AgentEvent` values into Vercel AI Data Protocol SSE frames.
|
|
20
|
+
|
|
21
|
+
Tracks open text/reasoning/tool-input blocks so that ``*-start`` and ``*-end``
|
|
22
|
+
frames are emitted correctly around delta streams.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
"""Initialise with no active blocks."""
|
|
27
|
+
self._active_text_id: str | None = None
|
|
28
|
+
self._active_reasoning_id: str | None = None
|
|
29
|
+
self._active_tool_input_id: str | None = None
|
|
30
|
+
|
|
31
|
+
def encode(self, agent_event: AgentEvent) -> Iterator[str]:
|
|
32
|
+
"""Encode a single :class:`~deepagents_serve.events.AgentEvent` as one or more SSE data lines."""
|
|
33
|
+
for part in self._to_parts(agent_event):
|
|
34
|
+
yield f"data: {json.dumps(dataclasses.asdict(part))}\n\n"
|
|
35
|
+
|
|
36
|
+
def flush(self) -> Iterator[str]:
|
|
37
|
+
"""Close any open text/reasoning blocks after the stream ends."""
|
|
38
|
+
for part in self._flush_parts():
|
|
39
|
+
yield f"data: {json.dumps(dataclasses.asdict(part))}\n\n"
|
|
40
|
+
|
|
41
|
+
def _to_parts(self, agent_event: AgentEvent) -> Iterator[protocol.StreamPart]: # noqa: C901
|
|
42
|
+
match agent_event:
|
|
43
|
+
case events.TextDelta(id=id, content=content):
|
|
44
|
+
if id != self._active_text_id:
|
|
45
|
+
if self._active_text_id is not None:
|
|
46
|
+
yield protocol.TextEnd(id=self._active_text_id)
|
|
47
|
+
yield protocol.TextStart(id=id)
|
|
48
|
+
self._active_text_id = id
|
|
49
|
+
yield protocol.TextDelta(id=id, delta=content)
|
|
50
|
+
|
|
51
|
+
case events.ReasoningDelta(id=id, content=content):
|
|
52
|
+
if id != self._active_reasoning_id:
|
|
53
|
+
if self._active_reasoning_id is not None:
|
|
54
|
+
yield protocol.ReasoningEnd(id=self._active_reasoning_id)
|
|
55
|
+
yield protocol.ReasoningStart(id=id)
|
|
56
|
+
self._active_reasoning_id = id
|
|
57
|
+
yield protocol.ReasoningDelta(id=id, delta=content)
|
|
58
|
+
|
|
59
|
+
case events.ToolInputDelta(id=id, name=name, args_delta=args_delta):
|
|
60
|
+
if id != self._active_tool_input_id:
|
|
61
|
+
yield protocol.ToolInputStart(toolCallId=id, toolName=name)
|
|
62
|
+
self._active_tool_input_id = id
|
|
63
|
+
yield protocol.ToolInputDelta(toolCallId=id, inputTextDelta=args_delta)
|
|
64
|
+
|
|
65
|
+
case events.ToolOutputAvailable(id=id, output=output):
|
|
66
|
+
yield protocol.ToolOutputAvailable(toolCallId=id, output=output)
|
|
67
|
+
|
|
68
|
+
case events.Error(text=text):
|
|
69
|
+
yield protocol.ErrorPart(errorText=text)
|
|
70
|
+
|
|
71
|
+
case events.Abort(reason=reason):
|
|
72
|
+
yield protocol.AbortPart(reason=reason)
|
|
73
|
+
|
|
74
|
+
def _flush_parts(self) -> Iterator[protocol.StreamPart]:
|
|
75
|
+
if self._active_text_id is not None:
|
|
76
|
+
yield protocol.TextEnd(id=self._active_text_id)
|
|
77
|
+
self._active_text_id = None
|
|
78
|
+
if self._active_reasoning_id is not None:
|
|
79
|
+
yield protocol.ReasoningEnd(id=self._active_reasoning_id)
|
|
80
|
+
self._active_reasoning_id = None
|
|
81
|
+
self._active_tool_input_id = None
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Vercel AI Data Protocol stream-part dataclasses.
|
|
2
|
+
|
|
3
|
+
Field names are intentionally camelCase to match the JSON field names defined by the
|
|
4
|
+
`Vercel AI Data Protocol <https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol>`_.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class MessageStart:
|
|
15
|
+
"""Signals the start of a new AI message."""
|
|
16
|
+
|
|
17
|
+
messageId: str
|
|
18
|
+
type: str = field(default="start", init=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class TextStart:
|
|
23
|
+
"""Signals the start of a text content block."""
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
type: str = field(default="text-start", init=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class TextDelta:
|
|
31
|
+
"""A text content fragment."""
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
delta: str
|
|
35
|
+
type: str = field(default="text-delta", init=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class TextEnd:
|
|
40
|
+
"""Signals the end of a text content block."""
|
|
41
|
+
|
|
42
|
+
id: str
|
|
43
|
+
type: str = field(default="text-end", init=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class ReasoningStart:
|
|
48
|
+
"""Signals the start of a reasoning/thinking block."""
|
|
49
|
+
|
|
50
|
+
id: str
|
|
51
|
+
type: str = field(default="reasoning-start", init=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class ReasoningDelta:
|
|
56
|
+
"""A reasoning/thinking content fragment."""
|
|
57
|
+
|
|
58
|
+
id: str
|
|
59
|
+
delta: str
|
|
60
|
+
type: str = field(default="reasoning-delta", init=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class ReasoningEnd:
|
|
65
|
+
"""Signals the end of a reasoning/thinking block."""
|
|
66
|
+
|
|
67
|
+
id: str
|
|
68
|
+
type: str = field(default="reasoning-end", init=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class SourceUrl:
|
|
73
|
+
"""A source URL citation."""
|
|
74
|
+
|
|
75
|
+
sourceId: str
|
|
76
|
+
url: str
|
|
77
|
+
type: str = field(default="source-url", init=False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class SourceDocument:
|
|
82
|
+
"""A source document citation."""
|
|
83
|
+
|
|
84
|
+
sourceId: str
|
|
85
|
+
mediaType: str
|
|
86
|
+
title: str
|
|
87
|
+
type: str = field(default="source-document", init=False)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class FilePart:
|
|
92
|
+
"""A file attachment part."""
|
|
93
|
+
|
|
94
|
+
url: str
|
|
95
|
+
mediaType: str
|
|
96
|
+
type: str = field(default="file", init=False)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True, slots=True)
|
|
100
|
+
class ErrorPart:
|
|
101
|
+
"""A non-fatal error part."""
|
|
102
|
+
|
|
103
|
+
errorText: str
|
|
104
|
+
type: str = field(default="error", init=False)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class ToolInputStart:
|
|
109
|
+
"""Signals the start of a tool-call input stream."""
|
|
110
|
+
|
|
111
|
+
toolCallId: str
|
|
112
|
+
toolName: str
|
|
113
|
+
type: str = field(default="tool-input-start", init=False)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class ToolInputDelta:
|
|
118
|
+
"""A streamed fragment of a tool-call input JSON string."""
|
|
119
|
+
|
|
120
|
+
toolCallId: str
|
|
121
|
+
inputTextDelta: str
|
|
122
|
+
type: str = field(default="tool-input-delta", init=False)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class ToolInputAvailable:
|
|
127
|
+
"""The complete, parsed input for a tool call."""
|
|
128
|
+
|
|
129
|
+
toolCallId: str
|
|
130
|
+
toolName: str
|
|
131
|
+
input: Any
|
|
132
|
+
type: str = field(default="tool-input-available", init=False)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class ToolOutputAvailable:
|
|
137
|
+
"""The output of a completed tool call."""
|
|
138
|
+
|
|
139
|
+
toolCallId: str
|
|
140
|
+
output: Any
|
|
141
|
+
type: str = field(default="tool-output-available", init=False)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True)
|
|
145
|
+
class StartStep:
|
|
146
|
+
"""Signals the start of an agent reasoning step."""
|
|
147
|
+
|
|
148
|
+
type: str = field(default="start-step", init=False)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class FinishStep:
|
|
153
|
+
"""Signals the end of an agent reasoning step."""
|
|
154
|
+
|
|
155
|
+
type: str = field(default="finish-step", init=False)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class FinishMessage:
|
|
160
|
+
"""Signals that the full AI message is complete."""
|
|
161
|
+
|
|
162
|
+
type: str = field(default="finish", init=False)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class AbortPart:
|
|
167
|
+
"""Signals that the stream was aborted."""
|
|
168
|
+
|
|
169
|
+
reason: str | None = None
|
|
170
|
+
type: str = field(default="abort", init=False)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
StreamPart = (
|
|
174
|
+
MessageStart
|
|
175
|
+
| TextStart
|
|
176
|
+
| TextDelta
|
|
177
|
+
| TextEnd
|
|
178
|
+
| ReasoningStart
|
|
179
|
+
| ReasoningDelta
|
|
180
|
+
| ReasoningEnd
|
|
181
|
+
| SourceUrl
|
|
182
|
+
| SourceDocument
|
|
183
|
+
| FilePart
|
|
184
|
+
| ErrorPart
|
|
185
|
+
| ToolInputStart
|
|
186
|
+
| ToolInputDelta
|
|
187
|
+
| ToolInputAvailable
|
|
188
|
+
| ToolOutputAvailable
|
|
189
|
+
| StartStep
|
|
190
|
+
| FinishStep
|
|
191
|
+
| FinishMessage
|
|
192
|
+
| AbortPart
|
|
193
|
+
)
|
deepagents_serve/app.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""DeepAgentsServeApp — wraps a LangGraph graph and exposes it via Starlette."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from starlette.applications import Starlette
|
|
8
|
+
|
|
9
|
+
from deepagents_serve.route.health import router as health_router
|
|
10
|
+
from deepagents_serve.route.vercel import router as vercel_router
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeepAgentsServeApp:
|
|
14
|
+
"""Builds a Starlette ASGI application from any compiled LangGraph graph.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
graph: A compiled LangGraph graph (or any object with an ``astream`` method).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, graph: Any) -> None:
|
|
21
|
+
"""Initialise with a compiled LangGraph graph."""
|
|
22
|
+
self._graph = graph
|
|
23
|
+
|
|
24
|
+
def build(self) -> Starlette:
|
|
25
|
+
"""Assemble and return the Starlette ASGI application."""
|
|
26
|
+
app = Starlette(
|
|
27
|
+
routes=[
|
|
28
|
+
*health_router.routes,
|
|
29
|
+
*vercel_router.routes,
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
app.state.graph = self._graph
|
|
33
|
+
return app
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Canonical agent event types emitted by the normaliser."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class TextDelta:
|
|
8
|
+
"""A streamed text content fragment from an AI message."""
|
|
9
|
+
|
|
10
|
+
id: str
|
|
11
|
+
content: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ReasoningDelta:
|
|
16
|
+
"""A streamed reasoning/thinking fragment from an AI message."""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
content: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ToolInputDelta:
|
|
24
|
+
"""A streamed partial argument string for a tool call."""
|
|
25
|
+
|
|
26
|
+
id: str
|
|
27
|
+
name: str
|
|
28
|
+
args_delta: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class ToolOutputAvailable:
|
|
33
|
+
"""The complete output of a finished tool call."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
output: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Error:
|
|
41
|
+
"""A non-fatal error event from the agent."""
|
|
42
|
+
|
|
43
|
+
text: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Abort:
|
|
48
|
+
"""Signals that the agent stream was aborted."""
|
|
49
|
+
|
|
50
|
+
reason: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
AgentEvent = TextDelta | ReasoningDelta | ToolInputDelta | ToolOutputAvailable | Error | Abort
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Normalise LangGraph ``stream_mode='messages'`` output into :data:`AgentEvent` values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from langchain_core.messages import AIMessage, AIMessageChunk, ToolMessage
|
|
8
|
+
|
|
9
|
+
from deepagents_serve.events import (
|
|
10
|
+
AgentEvent,
|
|
11
|
+
TextDelta,
|
|
12
|
+
ToolInputDelta,
|
|
13
|
+
ToolOutputAvailable,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import AsyncIterator, Iterator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def normalize(stream: AsyncIterator[Any]) -> AsyncIterator[AgentEvent]:
|
|
21
|
+
"""Yield :class:`AgentEvent` values from a LangGraph messages stream."""
|
|
22
|
+
async for message, _metadata in stream:
|
|
23
|
+
for agent_event in _parse_message(message):
|
|
24
|
+
yield agent_event
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_message(message: Any) -> Iterator[AgentEvent]:
|
|
28
|
+
if isinstance(message, (AIMessage, AIMessageChunk)):
|
|
29
|
+
yield from _parse_ai_message(message)
|
|
30
|
+
elif isinstance(message, ToolMessage):
|
|
31
|
+
yield ToolOutputAvailable(id=message.tool_call_id, output=str(message.content))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_ai_message(message: AIMessage | AIMessageChunk) -> Iterator[AgentEvent]:
|
|
35
|
+
msg_id = message.id or "msg"
|
|
36
|
+
content = message.content
|
|
37
|
+
|
|
38
|
+
if isinstance(content, str):
|
|
39
|
+
if content:
|
|
40
|
+
yield TextDelta(id=msg_id, content=content)
|
|
41
|
+
elif isinstance(content, list):
|
|
42
|
+
for block in content:
|
|
43
|
+
if not isinstance(block, dict):
|
|
44
|
+
continue
|
|
45
|
+
if block.get("type") == "text" and block.get("text"):
|
|
46
|
+
yield TextDelta(id=msg_id, content=block["text"])
|
|
47
|
+
|
|
48
|
+
for tool_call in getattr(message, "tool_call_chunks", []):
|
|
49
|
+
if tool_call.get("args"):
|
|
50
|
+
yield ToolInputDelta(
|
|
51
|
+
id=tool_call["id"] or msg_id,
|
|
52
|
+
name=tool_call.get("name", ""),
|
|
53
|
+
args_delta=tool_call["args"],
|
|
54
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Starlette route definitions for the deepagents-serve endpoints."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Health and readiness check endpoints."""
|
|
2
|
+
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from starlette.responses import JSONResponse
|
|
5
|
+
from starlette.routing import Route, Router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def _health(_request: Request) -> JSONResponse:
|
|
9
|
+
return JSONResponse({"status": "ok"})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _ready(request: Request) -> JSONResponse:
|
|
13
|
+
graph = request.app.state.graph
|
|
14
|
+
if graph is None:
|
|
15
|
+
return JSONResponse({"status": "not ready"}, status_code=503)
|
|
16
|
+
return JSONResponse({"status": "ready"})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
router = Router(
|
|
20
|
+
routes=[
|
|
21
|
+
Route("/health", _health, methods=["GET"]),
|
|
22
|
+
Route("/ready", _ready, methods=["GET"]),
|
|
23
|
+
]
|
|
24
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Vercel AI Data Protocol streaming chat endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from starlette.responses import StreamingResponse
|
|
8
|
+
from starlette.routing import Route, Router
|
|
9
|
+
|
|
10
|
+
from deepagents_serve.adapter.vercel import VercelAIProtocolAdapter
|
|
11
|
+
from deepagents_serve.normaliser import normalize
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
|
|
16
|
+
from starlette.requests import Request
|
|
17
|
+
|
|
18
|
+
# TODO: formalise request schema with a dataclass/Pydantic model
|
|
19
|
+
# Expected shape: {"messages": [{"role": "user"|"assistant", "content": str}]}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _chat_stream(request: Request) -> StreamingResponse:
|
|
23
|
+
body = await request.json()
|
|
24
|
+
messages = body.get("messages", [])
|
|
25
|
+
graph = request.app.state.graph
|
|
26
|
+
|
|
27
|
+
async def event_generator() -> AsyncIterator[str]:
|
|
28
|
+
adapter = VercelAIProtocolAdapter()
|
|
29
|
+
stream = graph.astream({"messages": messages}, stream_mode="messages")
|
|
30
|
+
|
|
31
|
+
async for agent_event in normalize(stream):
|
|
32
|
+
for chunk in adapter.encode(agent_event):
|
|
33
|
+
yield chunk
|
|
34
|
+
|
|
35
|
+
for chunk in adapter.flush():
|
|
36
|
+
yield chunk
|
|
37
|
+
|
|
38
|
+
return StreamingResponse(
|
|
39
|
+
event_generator(),
|
|
40
|
+
media_type="text/event-stream",
|
|
41
|
+
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
router = Router(
|
|
46
|
+
routes=[
|
|
47
|
+
Route("/chat/stream", _chat_stream, methods=["POST"]),
|
|
48
|
+
]
|
|
49
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepagents_serve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/FengJi2021/deepagents-serve
|
|
7
|
+
Project-URL: Repository, https://github.com/FengJi2021/deepagents-serve
|
|
8
|
+
Project-URL: Issues, https://github.com/FengJi2021/deepagents-serve/issues
|
|
9
|
+
Keywords: agents,ai,llm,langgraph,langchain,agent-server,vercel-ai,streaming
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: <4.0,>=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: starlette>=0.46.0
|
|
22
|
+
Requires-Dist: uvicorn>=0.34.0
|
|
23
|
+
Requires-Dist: langgraph>=0.4.0
|
|
24
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
25
|
+
|
|
26
|
+
# deepagents-serve
|
|
27
|
+
|
|
28
|
+
Universal agent server. Wraps any LangGraph graph and exposes it over standard agent protocols — Vercel AI SDK, Claude Code, TUIs, and more.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from deepagents_serve import DeepAgentsServeApp
|
|
34
|
+
from deepagents import create_deep_agent
|
|
35
|
+
import uvicorn
|
|
36
|
+
|
|
37
|
+
agent = create_deep_agent(
|
|
38
|
+
model="anthropic:claude-opus-4-8",
|
|
39
|
+
skills=["myskillsregistry/skills"],
|
|
40
|
+
system_prompt="You are a helpful assistant."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
app = DeepAgentsServeApp(agent)
|
|
44
|
+
|
|
45
|
+
uvicorn.run(app.build(), host="0.0.0.0", port=8000)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Endpoint design
|
|
51
|
+
|
|
52
|
+
### `POST /chat/stream`
|
|
53
|
+
|
|
54
|
+
Stateless. One request, one SSE stream, done. No session state is kept between calls.
|
|
55
|
+
|
|
56
|
+
**Request**
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"messages": [
|
|
61
|
+
{"role": "user", "content": "List the files in the working directory."}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Response** — `Content-Type: text/event-stream`
|
|
67
|
+
|
|
68
|
+
Uses the [Vercel AI Data Protocol](https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol). Each line is `data: <json>`:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
data: {"type": "text-start", "id": "msg_01"}
|
|
72
|
+
data: {"type": "text-delta", "id": "msg_01", "delta": "Here are the files:\n"}
|
|
73
|
+
data: {"type": "text-end", "id": "msg_01"}
|
|
74
|
+
|
|
75
|
+
data: {"type": "tool-input-start", "toolCallId": "tc_01", "toolName": "bash"}
|
|
76
|
+
data: {"type": "tool-input-delta", "toolCallId": "tc_01", "inputTextDelta": "{\"command\":\"ls\"}"}
|
|
77
|
+
data: {"type": "tool-output-available", "toolCallId": "tc_01", "output": "README.md\nsrc/"}
|
|
78
|
+
|
|
79
|
+
data: {"type": "reasoning-start", "id": "r_01"}
|
|
80
|
+
data: {"type": "reasoning-delta", "id": "r_01", "delta": "I should list the files..."}
|
|
81
|
+
data: {"type": "reasoning-end", "id": "r_01"}
|
|
82
|
+
|
|
83
|
+
data: {"type": "error", "errorText": "something went wrong"}
|
|
84
|
+
data: {"type": "abort", "reason": "user cancelled"}
|
|
85
|
+
data: {"type": "finish"}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Signal diagram
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
Client (Vercel SDK / Claude Code / TUI)
|
|
94
|
+
│
|
|
95
|
+
│ POST /chat/stream
|
|
96
|
+
│ {"messages": [{"role": "user", "content": "..."}]}
|
|
97
|
+
▼
|
|
98
|
+
┌──────────────────┐
|
|
99
|
+
│ Input Parser │ extract last user message from messages array
|
|
100
|
+
└────────┬─────────┘
|
|
101
|
+
│ {"role": "user", "content": "..."}
|
|
102
|
+
▼
|
|
103
|
+
┌──────────────────┐
|
|
104
|
+
│ LangGraph Graph │ graph.astream({"messages": [...]}, stream_mode=["messages"])
|
|
105
|
+
└────────┬─────────┘
|
|
106
|
+
│ (stream_part, metadata) chunks
|
|
107
|
+
▼
|
|
108
|
+
┌──────────────────┐
|
|
109
|
+
│ Normaliser │ LangGraph messages → canonical AgentEvent
|
|
110
|
+
│ │
|
|
111
|
+
│ AIMessage │→ TextDelta | ReasoningDelta
|
|
112
|
+
│ ToolCall │→ ToolInputDelta
|
|
113
|
+
│ ToolMessage │→ ToolOutputAvailable
|
|
114
|
+
│ Error │→ Error | Abort
|
|
115
|
+
└────────┬─────────┘
|
|
116
|
+
│ AgentEvent
|
|
117
|
+
▼
|
|
118
|
+
┌──────────────────┐
|
|
119
|
+
│ Vercel Adapter │ AgentEvent → Vercel SSE stream parts
|
|
120
|
+
└────────┬─────────┘
|
|
121
|
+
│ text/event-stream
|
|
122
|
+
▼
|
|
123
|
+
Client
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Roadmap
|
|
129
|
+
|
|
130
|
+
**Near-term**
|
|
131
|
+
|
|
132
|
+
- [ ] Input parsing — read request body, extract messages
|
|
133
|
+
- [ ] Complete normaliser — tool calls, tool results, reasoning deltas from LangGraph
|
|
134
|
+
- [ ] Request validation — reject malformed payloads at the boundary
|
|
135
|
+
|
|
136
|
+
**Protocol compatibility**
|
|
137
|
+
|
|
138
|
+
- [ ] Claude Managed Agents endpoints — `/v1/sessions`, `/v1/sessions/{id}/events`, `/v1/sessions/{id}/events/stream`
|
|
139
|
+
- [ ] Claude Managed Agents SSE encoding — `agent.message`, `agent.tool_use`, `session.status_idle` event types
|
|
140
|
+
- [ ] Agent metadata endpoint — `GET /v1/agents/default`
|
|
141
|
+
|
|
142
|
+
**Statefulness**
|
|
143
|
+
|
|
144
|
+
- [ ] Session layer — LangGraph `thread_id` checkpointing, `session_id` → `thread_id` mapping
|
|
145
|
+
- [ ] Multi-turn conversations — persist conversation history across requests
|
|
146
|
+
- [ ] Interrupt support — `user.interrupt` event stops graph mid-execution
|
|
147
|
+
|
|
148
|
+
**Deployment**
|
|
149
|
+
|
|
150
|
+
- [ ] kagent packaging — Kubernetes-compatible worker pattern
|
|
151
|
+
|
|
152
|
+
**Eval**
|
|
153
|
+
|
|
154
|
+
- [ ] Event history capture — persist full turn events with timestamps and token counts
|
|
155
|
+
- [ ] Session replay — re-run a captured session for regression testing
|
|
156
|
+
|
|
157
|
+
**Skills**
|
|
158
|
+
|
|
159
|
+
- [ ] Skill registry compatibility — discover and invoke skills from a registry
|
|
160
|
+
- [ ] Finetune on skills — export session history as structured training data
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
deepagents_serve/__init__.py,sha256=A3vL6So1kMaK8eEdge5-gm4bTrWHwGPP3Pk4rRm0Mvo,156
|
|
2
|
+
deepagents_serve/app.py,sha256=fFYqbgTk4c8OH1XOX4WvVmizkYwUKaUmFvkoYd8oKOs,970
|
|
3
|
+
deepagents_serve/events.py,sha256=olZlMvgnasEfCjNpTHm5TDBW7DF0_PSSFLxGabrybgw,1069
|
|
4
|
+
deepagents_serve/normaliser.py,sha256=MYuDrqLXE58JKas9MflnV0ntfioJnS7ElsyHAAJp7K4,1824
|
|
5
|
+
deepagents_serve/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
deepagents_serve/adapter/__init__.py,sha256=Y5ZhfkOQ1ivKNrwA4y4Zm6QuxnB7ZepV5Ns_sMFvUJg,114
|
|
7
|
+
deepagents_serve/adapter/vercel.py,sha256=zJzrStfqFhVJ3HN31gxYxmOXG5526TBXEd7bL0e01Hk,3558
|
|
8
|
+
deepagents_serve/adapter/vercel_protocol.py,sha256=f9TnJbAJ64we0WuKR77aZoTggV2FnPJvw5iXqradkWU,4287
|
|
9
|
+
deepagents_serve/route/__init__.py,sha256=T9nPIr6_9bL0b35SsxZR2UXqz2bSYiApxFmimcLAxwM,70
|
|
10
|
+
deepagents_serve/route/health.py,sha256=LOulB2C9giIcpDJkfmq3ErbN848hb5XUj9PvLHKptUg,640
|
|
11
|
+
deepagents_serve/route/vercel.py,sha256=DnqdTptUT3vHL2ZXYsxYJaXONHRyVJE7xj-58pKysOw,1424
|
|
12
|
+
deepagents_serve-0.1.0.dist-info/METADATA,sha256=F0MQ69r8A9gCGxxMztRESZvbplnXOvtpx0OggdIFv6w,5622
|
|
13
|
+
deepagents_serve-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
deepagents_serve-0.1.0.dist-info/top_level.txt,sha256=BAx7uyQaeFCbDLF0dQWN49ml4NBHjaKv8L5Q6yL9FAs,17
|
|
15
|
+
deepagents_serve-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepagents_serve
|