lc-agent-app 0.2.1__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.
- lc_agent/__init__.py +21 -0
- lc_agent/__main__.py +4 -0
- lc_agent/app.py +194 -0
- lc_agent/config/__init__.py +3 -0
- lc_agent/config/loader.py +86 -0
- lc_agent/config/schema.py +48 -0
- lc_agent/core/__init__.py +5 -0
- lc_agent/core/chat_model.py +48 -0
- lc_agent/core/engine.py +328 -0
- lc_agent/core/http_trace.py +228 -0
- lc_agent/core/http_trace_httpx.py +121 -0
- lc_agent/core/models.py +39 -0
- lc_agent/db/__init__.py +13 -0
- lc_agent/db/engine.py +76 -0
- lc_agent/db/migrations/README +1 -0
- lc_agent/db/migrations/env.py +72 -0
- lc_agent/db/migrations/script.py.mako +29 -0
- lc_agent/db/migrations/versions/20260623_add_http_traces_to_chat_ui_messages.py +25 -0
- lc_agent/db/migrations/versions/a342dc61a740_initial_schema.py +75 -0
- lc_agent/db/models.py +52 -0
- lc_agent/db/repository.py +164 -0
- lc_agent/main.py +24 -0
- lc_agent/mcp/__init__.py +3 -0
- lc_agent/mcp/manager.py +323 -0
- lc_agent/mcp/tool_adapter.py +98 -0
- lc_agent/server/__init__.py +4 -0
- lc_agent/server/app.py +57 -0
- lc_agent/server/dependencies.py +26 -0
- lc_agent/server/routes/__init__.py +1 -0
- lc_agent/server/routes/agents.py +249 -0
- lc_agent/server/routes/health.py +17 -0
- lc_agent/server/routes/mcp.py +49 -0
- lc_agent/server/routes/models.py +20 -0
- lc_agent/server/routes/sessions.py +136 -0
- lc_agent/server/routes/skills.py +63 -0
- lc_agent/server/routes/tools.py +71 -0
- lc_agent/server/websocket.py +485 -0
- lc_agent/skills/__init__.py +3 -0
- lc_agent/skills/scanner.py +111 -0
- lc_agent/tools/__init__.py +4 -0
- lc_agent/tools/builtin.py +10 -0
- lc_agent/tools/registry.py +139 -0
- lc_agent/utils/loggers.py +6 -0
- lc_agent/web/dist/assets/TestSegments-B24ou3vL.css +1 -0
- lc_agent/web/dist/assets/TestSegments-DoB1Aned.js +5 -0
- lc_agent/web/dist/assets/index-Bc9lZYfU.js +107 -0
- lc_agent/web/dist/assets/index-CQJEFcQz.css +1 -0
- lc_agent/web/dist/favicon.svg +22 -0
- lc_agent/web/dist/index.html +14 -0
- lc_agent_app-0.2.1.dist-info/METADATA +28 -0
- lc_agent_app-0.2.1.dist-info/RECORD +54 -0
- lc_agent_app-0.2.1.dist-info/WHEEL +4 -0
- lc_agent_app-0.2.1.dist-info/entry_points.txt +2 -0
- lc_agent_app-0.2.1.dist-info/licenses/LICENSE +21 -0
lc_agent/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# lc_agent/__init__.py
|
|
2
|
+
"""lc_agent — LangChain Agent framework with built-in Web UI."""
|
|
3
|
+
|
|
4
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("lc-agent-app")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.0.0-dev"
|
|
10
|
+
|
|
11
|
+
from lc_agent.app import LcAgentApp
|
|
12
|
+
from lc_agent.config.loader import load_config
|
|
13
|
+
from lc_agent.tools.registry import ToolRegistry, tool
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"LcAgentApp",
|
|
17
|
+
"load_config",
|
|
18
|
+
"ToolRegistry",
|
|
19
|
+
"tool",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
lc_agent/__main__.py
ADDED
lc_agent/app.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# lc_agent/app.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
8
|
+
|
|
9
|
+
from lc_agent.core.engine import AgentEngine
|
|
10
|
+
from lc_agent.db.engine import init_db
|
|
11
|
+
from lc_agent.server.app import create_app, mount_static_files
|
|
12
|
+
from lc_agent.server.websocket import ChatWebSocketHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LcAgentApp:
|
|
16
|
+
"""Main application orchestrator — creates engine, server, and runs."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: dict, host: str = "127.0.0.1", port: int = 8000):
|
|
19
|
+
self.config = config
|
|
20
|
+
self.host = host
|
|
21
|
+
self.port = port
|
|
22
|
+
self._db_url = config.get("database", {}).get("url", "sqlite+aiosqlite:///./lc_agent_data.db")
|
|
23
|
+
self._checkpoint_path = config.get("database", {}).get("checkpoint_path", "./lc_agent_checkpoints.db")
|
|
24
|
+
self.engine = AgentEngine(config)
|
|
25
|
+
from lc_agent.skills.scanner import SkillScanner
|
|
26
|
+
skills_dir = config.get("skills", {}).get("directory", "./skills")
|
|
27
|
+
self.skill_scanner = SkillScanner(skills_dir)
|
|
28
|
+
self.skill_scanner.scan()
|
|
29
|
+
from lc_agent.mcp.manager import McpManager
|
|
30
|
+
mcp_config = config.get("mcp_servers", {})
|
|
31
|
+
self.mcp_manager = McpManager(mcp_config, on_state_change=self._on_mcp_state_change)
|
|
32
|
+
self.fastapi_app = create_app(config, lifespan=self._lifespan)
|
|
33
|
+
self.fastapi_app.state.mcp_manager = self.mcp_manager
|
|
34
|
+
self.fastapi_app.state.skill_scanner = self.skill_scanner
|
|
35
|
+
self.engine._skill_scanner = self.skill_scanner
|
|
36
|
+
self.engine._mcp_manager = self.mcp_manager
|
|
37
|
+
self.fastapi_app.state.engine = self.engine
|
|
38
|
+
self._ws_handler = ChatWebSocketHandler(self.engine, db_url=self._db_url)
|
|
39
|
+
self._setup_websocket_route()
|
|
40
|
+
mount_static_files(self.fastapi_app)
|
|
41
|
+
|
|
42
|
+
def _on_mcp_state_change(self):
|
|
43
|
+
self.engine._mcp_generation += 1
|
|
44
|
+
|
|
45
|
+
@asynccontextmanager
|
|
46
|
+
async def _lifespan(self, app: FastAPI):
|
|
47
|
+
"""FastAPI lifespan: startup and shutdown logic."""
|
|
48
|
+
import asyncio
|
|
49
|
+
|
|
50
|
+
await init_db(self._db_url)
|
|
51
|
+
try:
|
|
52
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
53
|
+
import aiosqlite
|
|
54
|
+
conn = await aiosqlite.connect(self._checkpoint_path)
|
|
55
|
+
saver = AsyncSqliteSaver(conn)
|
|
56
|
+
await saver.setup()
|
|
57
|
+
self.engine._checkpointer = saver
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"[Warning] Checkpoint saver setup failed, using None: {e}")
|
|
60
|
+
|
|
61
|
+
await self._load_presets_from_db()
|
|
62
|
+
|
|
63
|
+
async def _connect_mcp_background():
|
|
64
|
+
try:
|
|
65
|
+
await self.mcp_manager.connect_all()
|
|
66
|
+
connected = [s for s in self.mcp_manager.servers if s.status == "connected"]
|
|
67
|
+
if connected:
|
|
68
|
+
print(f"[MCP] Connected: {[s.name for s in connected]}")
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"[MCP] Background connection error: {e}")
|
|
71
|
+
|
|
72
|
+
asyncio.create_task(_connect_mcp_background())
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
await self.mcp_manager.shutdown()
|
|
77
|
+
|
|
78
|
+
def _setup_websocket_route(self):
|
|
79
|
+
import asyncio
|
|
80
|
+
|
|
81
|
+
async def _ws_loop(websocket: WebSocket, tid: str):
|
|
82
|
+
streaming_task: asyncio.Task | None = None
|
|
83
|
+
try:
|
|
84
|
+
while True:
|
|
85
|
+
if streaming_task and not streaming_task.done():
|
|
86
|
+
recv_coro = websocket.receive_json()
|
|
87
|
+
recv_task = asyncio.ensure_future(recv_coro)
|
|
88
|
+
done, _ = await asyncio.wait(
|
|
89
|
+
[recv_task, streaming_task],
|
|
90
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
91
|
+
)
|
|
92
|
+
if recv_task in done:
|
|
93
|
+
data = recv_task.result()
|
|
94
|
+
if data.get("type") == "cancel":
|
|
95
|
+
self._ws_handler._cancel_flags[tid] = True
|
|
96
|
+
if streaming_task in done:
|
|
97
|
+
streaming_task = None
|
|
98
|
+
if recv_task not in done:
|
|
99
|
+
recv_task.cancel()
|
|
100
|
+
try:
|
|
101
|
+
await recv_task
|
|
102
|
+
except (asyncio.CancelledError, Exception):
|
|
103
|
+
pass
|
|
104
|
+
else:
|
|
105
|
+
data = await websocket.receive_json()
|
|
106
|
+
msg_type = data.get("type", "message")
|
|
107
|
+
if msg_type == "cancel":
|
|
108
|
+
continue
|
|
109
|
+
streaming_task = asyncio.create_task(
|
|
110
|
+
self._ws_handler.handle_message(websocket, tid, data)
|
|
111
|
+
)
|
|
112
|
+
except WebSocketDisconnect:
|
|
113
|
+
if streaming_task and not streaming_task.done():
|
|
114
|
+
streaming_task.cancel()
|
|
115
|
+
await self._ws_handler.disconnect(tid)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"[WS] Loop error: {e}")
|
|
118
|
+
if streaming_task and not streaming_task.done():
|
|
119
|
+
streaming_task.cancel()
|
|
120
|
+
await self._ws_handler.disconnect(tid)
|
|
121
|
+
|
|
122
|
+
@self.fastapi_app.websocket("/ws/chat/{thread_id}")
|
|
123
|
+
async def websocket_chat(websocket: WebSocket, thread_id: str):
|
|
124
|
+
tid = await self._ws_handler.connect(websocket, thread_id)
|
|
125
|
+
await _ws_loop(websocket, tid)
|
|
126
|
+
|
|
127
|
+
@self.fastapi_app.websocket("/ws/chat")
|
|
128
|
+
async def websocket_chat_auto(websocket: WebSocket):
|
|
129
|
+
tid = await self._ws_handler.connect(websocket)
|
|
130
|
+
await _ws_loop(websocket, tid)
|
|
131
|
+
|
|
132
|
+
async def _load_presets_from_db(self):
|
|
133
|
+
"""Load user-created presets from database on startup."""
|
|
134
|
+
from lc_agent.db.engine import get_async_session
|
|
135
|
+
from lc_agent.db.models import AgentPresetDB
|
|
136
|
+
from lc_agent.core.models import AgentPreset
|
|
137
|
+
from sqlalchemy import select
|
|
138
|
+
|
|
139
|
+
session = get_async_session(self._db_url)
|
|
140
|
+
try:
|
|
141
|
+
stmt = select(AgentPresetDB)
|
|
142
|
+
result = await session.execute(stmt)
|
|
143
|
+
for row in result.scalars().all():
|
|
144
|
+
preset = AgentPreset(
|
|
145
|
+
id=row.id,
|
|
146
|
+
name=row.name,
|
|
147
|
+
system_prompt=row.system_prompt,
|
|
148
|
+
default_model=row.default_model,
|
|
149
|
+
allowed_tool_groups=row.allowed_tool_groups,
|
|
150
|
+
allowed_mcp_servers=row.allowed_mcp_servers,
|
|
151
|
+
allowed_skills=row.allowed_skills,
|
|
152
|
+
dangerous_tools=row.dangerous_tools,
|
|
153
|
+
)
|
|
154
|
+
self.engine._presets[preset.id] = preset
|
|
155
|
+
loaded = len(self.engine._presets)
|
|
156
|
+
if loaded:
|
|
157
|
+
print(f"[Agents] Loaded {loaded} user presets from database")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
print(f"[Warning] Failed to load presets from DB: {e}")
|
|
160
|
+
finally:
|
|
161
|
+
await session.close()
|
|
162
|
+
|
|
163
|
+
def add_agent(self, name: str, graph, description: str = ""):
|
|
164
|
+
"""Register a pre-built CompiledStateGraph as a named agent.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: Unique agent identifier
|
|
168
|
+
graph: A compiled LangGraph (must have ainvoke and astream_events)
|
|
169
|
+
description: Human-readable description
|
|
170
|
+
"""
|
|
171
|
+
if name in self.engine._agents:
|
|
172
|
+
raise ValueError(f"Agent '{name}' already registered")
|
|
173
|
+
|
|
174
|
+
from lc_agent.core.models import AgentPreset
|
|
175
|
+
|
|
176
|
+
self.engine._agents[name] = graph
|
|
177
|
+
self.engine._agent_mcp_gen[name] = self.engine._mcp_generation
|
|
178
|
+
preset = AgentPreset(
|
|
179
|
+
id=name,
|
|
180
|
+
name=name,
|
|
181
|
+
system_prompt=description or f"Custom agent: {name}",
|
|
182
|
+
default_model="custom",
|
|
183
|
+
source="code",
|
|
184
|
+
)
|
|
185
|
+
self.engine._custom_presets[name] = preset
|
|
186
|
+
|
|
187
|
+
def run(self):
|
|
188
|
+
"""Start the server (blocking)."""
|
|
189
|
+
from lc_agent import __version__
|
|
190
|
+
|
|
191
|
+
print(f"\n lc_agent v{__version__}")
|
|
192
|
+
print(f" Web UI: http://{self.host}:{self.port}")
|
|
193
|
+
print(f" API Docs: http://{self.host}:{self.port}/api/docs\n")
|
|
194
|
+
uvicorn.run(self.fastapi_app, host=self.host, port=self.port)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import commentjson
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
ENV_PATTERN = re.compile(r"\{env:([^}]+)\}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def substitute_env_vars(data: Any) -> Any:
|
|
13
|
+
"""Recursively replace {env:VAR_NAME} patterns with environment variable values."""
|
|
14
|
+
if isinstance(data, str):
|
|
15
|
+
match = ENV_PATTERN.fullmatch(data)
|
|
16
|
+
if match:
|
|
17
|
+
var_name = match.group(1)
|
|
18
|
+
value = os.environ.get(var_name)
|
|
19
|
+
if value is None:
|
|
20
|
+
raise ValueError(f"Environment variable '{var_name}' not found")
|
|
21
|
+
return value
|
|
22
|
+
def replacer(m):
|
|
23
|
+
var_name = m.group(1)
|
|
24
|
+
value = os.environ.get(var_name)
|
|
25
|
+
if value is None:
|
|
26
|
+
raise ValueError(f"Environment variable '{var_name}' not found")
|
|
27
|
+
return value
|
|
28
|
+
return ENV_PATTERN.sub(replacer, data)
|
|
29
|
+
elif isinstance(data, dict):
|
|
30
|
+
return {k: substitute_env_vars(v) for k, v in data.items()}
|
|
31
|
+
elif isinstance(data, list):
|
|
32
|
+
return [substitute_env_vars(item) for item in data]
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_config_from_file(path: str) -> dict:
|
|
37
|
+
"""Load a JSONC configuration file and apply env substitution."""
|
|
38
|
+
file_path = Path(path)
|
|
39
|
+
if not file_path.exists():
|
|
40
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
41
|
+
|
|
42
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
43
|
+
raw = commentjson.load(f)
|
|
44
|
+
|
|
45
|
+
return substitute_env_vars(raw)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_config(
|
|
49
|
+
config_path: str | None = None,
|
|
50
|
+
dotenv_path: str | None = None,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""Load configuration with priority: explicit path > ./config.jsonc > ~/.lc_agent/config.jsonc > defaults."""
|
|
53
|
+
if dotenv_path:
|
|
54
|
+
load_dotenv(dotenv_path)
|
|
55
|
+
else:
|
|
56
|
+
load_dotenv()
|
|
57
|
+
|
|
58
|
+
search_paths = []
|
|
59
|
+
if config_path:
|
|
60
|
+
search_paths.append(Path(config_path))
|
|
61
|
+
search_paths.append(Path.cwd() / "config.jsonc")
|
|
62
|
+
search_paths.append(Path.home() / ".lc_agent" / "config.jsonc")
|
|
63
|
+
|
|
64
|
+
for p in search_paths:
|
|
65
|
+
if p.exists():
|
|
66
|
+
config = load_config_from_file(str(p))
|
|
67
|
+
config["_config_path"] = str(p)
|
|
68
|
+
config["_project_root"] = str(p.parent)
|
|
69
|
+
return config
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"provider": {},
|
|
73
|
+
"agent": {
|
|
74
|
+
"system_prompt": "You are a helpful assistant.",
|
|
75
|
+
"default_model": "",
|
|
76
|
+
"streaming": True,
|
|
77
|
+
},
|
|
78
|
+
"database": {
|
|
79
|
+
"url": "sqlite+aiosqlite:///./lc_agent_data.db",
|
|
80
|
+
"checkpoint_path": "./lc_agent_checkpoints.db",
|
|
81
|
+
},
|
|
82
|
+
"skills": {"directory": "./skills"},
|
|
83
|
+
"mcp_servers": {},
|
|
84
|
+
"_config_path": None,
|
|
85
|
+
"_project_root": str(Path.cwd()),
|
|
86
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ModelConfig(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
context_limit: int = 8000
|
|
7
|
+
max_output_tokens: int = 0
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProviderConfig(BaseModel):
|
|
11
|
+
api_key: str = ""
|
|
12
|
+
base_url: str = ""
|
|
13
|
+
models: list[ModelConfig] = Field(default_factory=list)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DatabaseConfig(BaseModel):
|
|
17
|
+
url: str = "sqlite+aiosqlite:///./lc_agent_data.db"
|
|
18
|
+
checkpoint_path: str = "./lc_agent_checkpoints.db"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SkillsConfig(BaseModel):
|
|
22
|
+
directory: str = "./skills"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class McpServerConfig(BaseModel):
|
|
26
|
+
type: str = "local" # "local", "sse", "http"
|
|
27
|
+
command: str | list[str] = ""
|
|
28
|
+
args: list[str] = Field(default_factory=list)
|
|
29
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
30
|
+
url: str = ""
|
|
31
|
+
enabled: bool = True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AppConfig(BaseModel):
|
|
35
|
+
"""Application configuration schema."""
|
|
36
|
+
|
|
37
|
+
provider: dict[str, ProviderConfig | dict] = Field(default_factory=dict)
|
|
38
|
+
agent: dict = Field(default_factory=lambda: {
|
|
39
|
+
"system_prompt": "You are a helpful assistant.",
|
|
40
|
+
"default_model": "",
|
|
41
|
+
"streaming": True,
|
|
42
|
+
})
|
|
43
|
+
mcp: dict = Field(default_factory=dict)
|
|
44
|
+
database: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
|
45
|
+
session: dict = Field(default_factory=lambda: {"db_path": ""})
|
|
46
|
+
ui: dict = Field(default_factory=dict)
|
|
47
|
+
skills: SkillsConfig = Field(default_factory=SkillsConfig)
|
|
48
|
+
mcp_servers: dict[str, McpServerConfig] = Field(default_factory=dict)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""ChatOpenAI subclass that extracts reasoning_content from streaming deltas.
|
|
2
|
+
|
|
3
|
+
ChatOpenAI only supports official OpenAI API fields. Many providers (DeepSeek,
|
|
4
|
+
GLM, etc.) return a non-standard `reasoning_content` field in the streaming
|
|
5
|
+
delta for chain-of-thought / thinking content. This subclass captures it into
|
|
6
|
+
`additional_kwargs["reasoning_content"]` so downstream handlers can display it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from langchain_core.messages import AIMessageChunk
|
|
12
|
+
from langchain_core.outputs import ChatGenerationChunk
|
|
13
|
+
from langchain_openai import ChatOpenAI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatOpenAIReasoning(ChatOpenAI):
|
|
17
|
+
"""ChatOpenAI with reasoning_content extraction from streaming deltas.
|
|
18
|
+
|
|
19
|
+
Drop-in replacement for ChatOpenAI. Works with any provider that returns
|
|
20
|
+
`reasoning_content` or `reasoning` in the streaming delta dict (e.g.
|
|
21
|
+
DeepSeek, GLM with thinking mode, OpenRouter).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def _convert_chunk_to_generation_chunk(
|
|
25
|
+
self,
|
|
26
|
+
chunk: dict,
|
|
27
|
+
default_chunk_class: type,
|
|
28
|
+
base_generation_info: dict | None,
|
|
29
|
+
) -> ChatGenerationChunk | None:
|
|
30
|
+
generation_chunk = super()._convert_chunk_to_generation_chunk(
|
|
31
|
+
chunk, default_chunk_class, base_generation_info,
|
|
32
|
+
)
|
|
33
|
+
if generation_chunk is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
choices = chunk.get("choices") or chunk.get("chunk", {}).get("choices") or []
|
|
37
|
+
if not choices:
|
|
38
|
+
return generation_chunk
|
|
39
|
+
|
|
40
|
+
delta = choices[0].get("delta") or {}
|
|
41
|
+
if isinstance(generation_chunk.message, AIMessageChunk):
|
|
42
|
+
reasoning = delta.get("reasoning_content")
|
|
43
|
+
if reasoning is None:
|
|
44
|
+
reasoning = delta.get("reasoning")
|
|
45
|
+
if reasoning is not None:
|
|
46
|
+
generation_chunk.message.additional_kwargs["reasoning_content"] = reasoning
|
|
47
|
+
|
|
48
|
+
return generation_chunk
|