lionagi 0.14.8__py3-none-any.whl → 0.14.10__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.
- lionagi/_errors.py +120 -11
- lionagi/_types.py +0 -6
- lionagi/config.py +3 -1
- lionagi/fields/reason.py +1 -1
- lionagi/libs/concurrency/throttle.py +79 -0
- lionagi/libs/parse.py +2 -1
- lionagi/libs/unstructured/__init__.py +0 -0
- lionagi/libs/unstructured/pdf_to_image.py +45 -0
- lionagi/libs/unstructured/read_image_to_base64.py +33 -0
- lionagi/libs/validate/to_num.py +378 -0
- lionagi/libs/validate/xml_parser.py +203 -0
- lionagi/models/operable_model.py +8 -3
- lionagi/operations/flow.py +0 -1
- lionagi/protocols/generic/event.py +2 -0
- lionagi/protocols/generic/log.py +26 -10
- lionagi/protocols/operatives/step.py +1 -1
- lionagi/protocols/types.py +9 -1
- lionagi/service/__init__.py +22 -1
- lionagi/service/connections/api_calling.py +57 -2
- lionagi/service/connections/endpoint_config.py +1 -1
- lionagi/service/connections/header_factory.py +4 -2
- lionagi/service/connections/match_endpoint.py +10 -10
- lionagi/service/connections/providers/anthropic_.py +5 -2
- lionagi/service/connections/providers/claude_code_.py +13 -17
- lionagi/service/connections/providers/claude_code_cli.py +51 -16
- lionagi/service/connections/providers/exa_.py +5 -3
- lionagi/service/connections/providers/oai_.py +116 -81
- lionagi/service/connections/providers/ollama_.py +38 -18
- lionagi/service/connections/providers/perplexity_.py +36 -14
- lionagi/service/connections/providers/types.py +30 -0
- lionagi/service/hooks/__init__.py +25 -0
- lionagi/service/hooks/_types.py +52 -0
- lionagi/service/hooks/_utils.py +85 -0
- lionagi/service/hooks/hook_event.py +67 -0
- lionagi/service/hooks/hook_registry.py +221 -0
- lionagi/service/imodel.py +120 -34
- lionagi/service/third_party/claude_code.py +715 -0
- lionagi/service/third_party/openai_model_names.py +198 -0
- lionagi/service/third_party/pplx_models.py +16 -8
- lionagi/service/types.py +21 -0
- lionagi/session/branch.py +1 -4
- lionagi/tools/base.py +1 -3
- lionagi/tools/file/reader.py +1 -1
- lionagi/tools/memory/tools.py +2 -2
- lionagi/utils.py +12 -775
- lionagi/version.py +1 -1
- {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/METADATA +6 -2
- {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/RECORD +50 -40
- lionagi/service/connections/providers/_claude_code/__init__.py +0 -3
- lionagi/service/connections/providers/_claude_code/models.py +0 -244
- lionagi/service/connections/providers/_claude_code/stream_cli.py +0 -359
- lionagi/service/third_party/openai_models.py +0 -18241
- {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/WHEEL +0 -0
- {lionagi-0.14.8.dist-info → lionagi-0.14.10.dist-info}/licenses/LICENSE +0 -0
@@ -1,359 +0,0 @@
|
|
1
|
-
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
-
#
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from __future__ import annotations
|
6
|
-
|
7
|
-
import asyncio
|
8
|
-
import codecs
|
9
|
-
import contextlib
|
10
|
-
import dataclasses
|
11
|
-
import json
|
12
|
-
import logging
|
13
|
-
import shutil
|
14
|
-
from collections.abc import AsyncIterator, Callable
|
15
|
-
from datetime import datetime
|
16
|
-
from functools import partial
|
17
|
-
from textwrap import shorten
|
18
|
-
from typing import Any
|
19
|
-
|
20
|
-
from json_repair import repair_json
|
21
|
-
|
22
|
-
from lionagi.libs.schema.as_readable import as_readable
|
23
|
-
from lionagi.utils import is_coro_func
|
24
|
-
|
25
|
-
from .models import ClaudeCodeRequest
|
26
|
-
|
27
|
-
CLAUDE = shutil.which("claude") or "claude"
|
28
|
-
if not shutil.which(CLAUDE):
|
29
|
-
raise RuntimeError(
|
30
|
-
"Claude CLI binary not found (npm i -g @anthropic-ai/claude-code)"
|
31
|
-
)
|
32
|
-
logging.basicConfig(level=logging.INFO)
|
33
|
-
log = logging.getLogger("claude-cli")
|
34
|
-
|
35
|
-
|
36
|
-
@dataclasses.dataclass
|
37
|
-
class ClaudeChunk:
|
38
|
-
"""Low-level wrapper around every NDJSON object coming from the CLI."""
|
39
|
-
|
40
|
-
raw: dict[str, Any]
|
41
|
-
type: str
|
42
|
-
# convenience views
|
43
|
-
thinking: str | None = None
|
44
|
-
text: str | None = None
|
45
|
-
tool_use: dict[str, Any] | None = None
|
46
|
-
tool_result: dict[str, Any] | None = None
|
47
|
-
|
48
|
-
|
49
|
-
@dataclasses.dataclass
|
50
|
-
class ClaudeSession:
|
51
|
-
"""Aggregated view of a whole CLI conversation."""
|
52
|
-
|
53
|
-
session_id: str | None = None
|
54
|
-
model: str | None = None
|
55
|
-
|
56
|
-
# chronological log
|
57
|
-
chunks: list[ClaudeChunk] = dataclasses.field(default_factory=list)
|
58
|
-
|
59
|
-
# materialised views
|
60
|
-
thinking_log: list[str] = dataclasses.field(default_factory=list)
|
61
|
-
messages: list[dict[str, Any]] = dataclasses.field(default_factory=list)
|
62
|
-
tool_uses: list[dict[str, Any]] = dataclasses.field(default_factory=list)
|
63
|
-
tool_results: list[dict[str, Any]] = dataclasses.field(
|
64
|
-
default_factory=list
|
65
|
-
)
|
66
|
-
|
67
|
-
# final summary
|
68
|
-
result: str = ""
|
69
|
-
usage: dict[str, Any] = dataclasses.field(default_factory=dict)
|
70
|
-
total_cost_usd: float | None = None
|
71
|
-
num_turns: int | None = None
|
72
|
-
duration_ms: int | None = None
|
73
|
-
duration_api_ms: int | None = None
|
74
|
-
is_error: bool = False
|
75
|
-
|
76
|
-
|
77
|
-
# --------------------------------------------------------------------------- helpers
|
78
|
-
|
79
|
-
|
80
|
-
async def ndjson_from_cli(request: ClaudeCodeRequest):
|
81
|
-
"""
|
82
|
-
Yields each JSON object emitted by the *claude-code* CLI.
|
83
|
-
|
84
|
-
• Robust against UTF-8 splits across chunks (incremental decoder).
|
85
|
-
• Robust against braces inside strings (uses json.JSONDecoder.raw_decode)
|
86
|
-
• Falls back to `json_repair.repair_json` when necessary.
|
87
|
-
"""
|
88
|
-
workspace = request.cwd()
|
89
|
-
workspace.mkdir(parents=True, exist_ok=True)
|
90
|
-
|
91
|
-
proc = await asyncio.create_subprocess_exec(
|
92
|
-
CLAUDE,
|
93
|
-
*request.as_cmd_args(),
|
94
|
-
cwd=str(workspace),
|
95
|
-
stdout=asyncio.subprocess.PIPE,
|
96
|
-
stderr=asyncio.subprocess.PIPE,
|
97
|
-
)
|
98
|
-
|
99
|
-
decoder = codecs.getincrementaldecoder("utf-8")()
|
100
|
-
json_decoder = json.JSONDecoder()
|
101
|
-
buffer: str = "" # text buffer that may hold >1 JSON objects
|
102
|
-
|
103
|
-
try:
|
104
|
-
while True:
|
105
|
-
chunk = await proc.stdout.read(4096)
|
106
|
-
if not chunk:
|
107
|
-
break
|
108
|
-
|
109
|
-
# 1) decode *incrementally* so we never split multibyte chars
|
110
|
-
buffer += decoder.decode(chunk)
|
111
|
-
|
112
|
-
# 2) try to peel off as many complete JSON objs as possible
|
113
|
-
while buffer:
|
114
|
-
buffer = buffer.lstrip() # remove leading spaces/newlines
|
115
|
-
if not buffer:
|
116
|
-
break
|
117
|
-
try:
|
118
|
-
obj, idx = json_decoder.raw_decode(buffer)
|
119
|
-
yield obj
|
120
|
-
buffer = buffer[idx:] # keep remainder for next round
|
121
|
-
except json.JSONDecodeError:
|
122
|
-
# incomplete → need more bytes
|
123
|
-
break
|
124
|
-
|
125
|
-
# 3) flush any tail bytes in the incremental decoder
|
126
|
-
buffer += decoder.decode(b"", final=True)
|
127
|
-
buffer = buffer.strip()
|
128
|
-
if buffer:
|
129
|
-
try:
|
130
|
-
obj, idx = json_decoder.raw_decode(buffer)
|
131
|
-
yield obj
|
132
|
-
except json.JSONDecodeError:
|
133
|
-
try:
|
134
|
-
fixed = repair_json(buffer)
|
135
|
-
yield json.loads(fixed)
|
136
|
-
log.warning(
|
137
|
-
"Repaired malformed JSON fragment at stream end"
|
138
|
-
)
|
139
|
-
except Exception:
|
140
|
-
log.error(
|
141
|
-
"Skipped unrecoverable JSON tail: %.120s…", buffer
|
142
|
-
)
|
143
|
-
|
144
|
-
# 4) propagate non-zero exit code
|
145
|
-
if await proc.wait() != 0:
|
146
|
-
err = (await proc.stderr.read()).decode().strip()
|
147
|
-
raise RuntimeError(err or "CLI exited non-zero")
|
148
|
-
|
149
|
-
finally:
|
150
|
-
with contextlib.suppress(ProcessLookupError):
|
151
|
-
proc.terminate()
|
152
|
-
await proc.wait()
|
153
|
-
|
154
|
-
|
155
|
-
# --------------------------------------------------------------------------- SSE route
|
156
|
-
async def stream_events(request: ClaudeCodeRequest):
|
157
|
-
async for obj in ndjson_from_cli(request):
|
158
|
-
yield obj
|
159
|
-
yield {"type": "done"}
|
160
|
-
|
161
|
-
|
162
|
-
print_readable = partial(as_readable, md=True, display_str=True)
|
163
|
-
|
164
|
-
|
165
|
-
def _pp_system(sys_obj: dict[str, Any], theme) -> None:
|
166
|
-
txt = (
|
167
|
-
f"◼️ **Claude Code Session** \n"
|
168
|
-
f"- id: `{sys_obj.get('session_id', '?')}` \n"
|
169
|
-
f"- model: `{sys_obj.get('model', '?')}` \n"
|
170
|
-
f"- tools: {', '.join(sys_obj.get('tools', [])[:8])}"
|
171
|
-
+ ("…" if len(sys_obj.get("tools", [])) > 8 else "")
|
172
|
-
)
|
173
|
-
print_readable(txt, border=False, theme=theme)
|
174
|
-
|
175
|
-
|
176
|
-
def _pp_thinking(thought: str, theme) -> None:
|
177
|
-
text = f"""
|
178
|
-
🧠 Thinking:
|
179
|
-
{thought}
|
180
|
-
"""
|
181
|
-
print_readable(text, border=True, theme=theme)
|
182
|
-
|
183
|
-
|
184
|
-
def _pp_assistant_text(text: str, theme) -> None:
|
185
|
-
txt = f"""
|
186
|
-
> 🗣️ Claude:
|
187
|
-
{text}
|
188
|
-
"""
|
189
|
-
print_readable(txt, theme=theme)
|
190
|
-
|
191
|
-
|
192
|
-
def _pp_tool_use(tu: dict[str, Any], theme) -> None:
|
193
|
-
preview = shorten(str(tu["input"]).replace("\n", " "), 130)
|
194
|
-
body = f"- 🔧 Tool Use — {tu['name']}({tu['id']}) - input: {preview}"
|
195
|
-
print_readable(body, border=False, panel=False, theme=theme)
|
196
|
-
|
197
|
-
|
198
|
-
def _pp_tool_result(tr: dict[str, Any], theme) -> None:
|
199
|
-
body_preview = shorten(str(tr["content"]).replace("\n", " "), 130)
|
200
|
-
status = "ERR" if tr.get("is_error") else "OK"
|
201
|
-
body = f"- 📄 Tool Result({tr['tool_use_id']}) - {status}\n\n\tcontent: {body_preview}"
|
202
|
-
print_readable(body, border=False, panel=False, theme=theme)
|
203
|
-
|
204
|
-
|
205
|
-
def _pp_final(sess: ClaudeSession, theme) -> None:
|
206
|
-
usage = sess.usage or {}
|
207
|
-
txt = (
|
208
|
-
f"### ✅ Session complete - {datetime.utcnow().isoformat(timespec='seconds')} UTC\n"
|
209
|
-
f"**Result:**\n\n{sess.result or ''}\n\n"
|
210
|
-
f"- cost: **${sess.total_cost_usd:.4f}** \n"
|
211
|
-
f"- turns: **{sess.num_turns}** \n"
|
212
|
-
f"- duration: **{sess.duration_ms} ms** (API {sess.duration_api_ms} ms) \n"
|
213
|
-
f"- tokens in/out: {usage.get('input_tokens', 0)}/{usage.get('output_tokens', 0)}"
|
214
|
-
)
|
215
|
-
print_readable(txt, theme=theme)
|
216
|
-
|
217
|
-
|
218
|
-
# --------------------------------------------------------------------------- internal utils
|
219
|
-
|
220
|
-
|
221
|
-
async def _maybe_await(func, *args, **kw):
|
222
|
-
"""Call func which may be sync or async."""
|
223
|
-
res = func(*args, **kw) if func else None
|
224
|
-
if is_coro_func(res):
|
225
|
-
await res
|
226
|
-
|
227
|
-
|
228
|
-
# --------------------------------------------------------------------------- main parser
|
229
|
-
|
230
|
-
|
231
|
-
async def stream_claude_code_cli( # noqa: C901 (complexity from branching is fine here)
|
232
|
-
request: ClaudeCodeRequest,
|
233
|
-
session: ClaudeSession = ClaudeSession(),
|
234
|
-
*,
|
235
|
-
on_system: Callable[[dict[str, Any]], None] | None = None,
|
236
|
-
on_thinking: Callable[[str], None] | None = None,
|
237
|
-
on_text: Callable[[str], None] | None = None,
|
238
|
-
on_tool_use: Callable[[dict[str, Any]], None] | None = None,
|
239
|
-
on_tool_result: Callable[[dict[str, Any]], None] | None = None,
|
240
|
-
on_final: Callable[[ClaudeSession], None] | None = None,
|
241
|
-
) -> AsyncIterator[ClaudeChunk | dict | ClaudeSession]:
|
242
|
-
"""
|
243
|
-
Consume the ND-JSON stream produced by ndjson_from_cli()
|
244
|
-
and return a fully-populated ClaudeSession.
|
245
|
-
|
246
|
-
If callbacks are omitted a default pretty-print is emitted.
|
247
|
-
"""
|
248
|
-
stream = ndjson_from_cli(request)
|
249
|
-
theme = request.cli_display_theme or "light"
|
250
|
-
|
251
|
-
async for obj in stream:
|
252
|
-
typ = obj.get("type", "unknown")
|
253
|
-
chunk = ClaudeChunk(raw=obj, type=typ)
|
254
|
-
session.chunks.append(chunk)
|
255
|
-
|
256
|
-
# ------------------------ SYSTEM -----------------------------------
|
257
|
-
if typ == "system":
|
258
|
-
data = obj
|
259
|
-
session.session_id = data.get("session_id", session.session_id)
|
260
|
-
session.model = data.get("model", session.model)
|
261
|
-
await _maybe_await(on_system, data)
|
262
|
-
if request.verbose_output:
|
263
|
-
_pp_system(data, theme)
|
264
|
-
yield data
|
265
|
-
|
266
|
-
# ------------------------ ASSISTANT --------------------------------
|
267
|
-
elif typ == "assistant":
|
268
|
-
msg = obj["message"]
|
269
|
-
session.messages.append(msg)
|
270
|
-
|
271
|
-
for blk in msg.get("content", []):
|
272
|
-
btype = blk.get("type")
|
273
|
-
if btype == "thinking":
|
274
|
-
thought = blk.get("thinking", "").strip()
|
275
|
-
chunk.thinking = thought
|
276
|
-
session.thinking_log.append(thought)
|
277
|
-
await _maybe_await(on_thinking, thought)
|
278
|
-
if request.verbose_output:
|
279
|
-
_pp_thinking(thought, theme)
|
280
|
-
|
281
|
-
elif btype == "text":
|
282
|
-
text = blk.get("text", "")
|
283
|
-
chunk.text = text
|
284
|
-
await _maybe_await(on_text, text)
|
285
|
-
if request.verbose_output:
|
286
|
-
_pp_assistant_text(text, theme)
|
287
|
-
|
288
|
-
elif btype == "tool_use":
|
289
|
-
tu = {
|
290
|
-
"id": blk["id"],
|
291
|
-
"name": blk["name"],
|
292
|
-
"input": blk["input"],
|
293
|
-
}
|
294
|
-
chunk.tool_use = tu
|
295
|
-
session.tool_uses.append(tu)
|
296
|
-
await _maybe_await(on_tool_use, tu)
|
297
|
-
if request.verbose_output:
|
298
|
-
_pp_tool_use(tu, theme)
|
299
|
-
|
300
|
-
elif btype == "tool_result":
|
301
|
-
tr = {
|
302
|
-
"tool_use_id": blk["tool_use_id"],
|
303
|
-
"content": blk["content"],
|
304
|
-
"is_error": blk.get("is_error", False),
|
305
|
-
}
|
306
|
-
chunk.tool_result = tr
|
307
|
-
session.tool_results.append(tr)
|
308
|
-
await _maybe_await(on_tool_result, tr)
|
309
|
-
if request.verbose_output:
|
310
|
-
_pp_tool_result(tr, theme)
|
311
|
-
yield chunk
|
312
|
-
|
313
|
-
# ------------------------ USER (tool_result containers) ------------
|
314
|
-
elif typ == "user":
|
315
|
-
msg = obj["message"]
|
316
|
-
session.messages.append(msg)
|
317
|
-
for blk in msg.get("content", []):
|
318
|
-
if blk.get("type") == "tool_result":
|
319
|
-
tr = {
|
320
|
-
"tool_use_id": blk["tool_use_id"],
|
321
|
-
"content": blk["content"],
|
322
|
-
"is_error": blk.get("is_error", False),
|
323
|
-
}
|
324
|
-
chunk.tool_result = tr
|
325
|
-
session.tool_results.append(tr)
|
326
|
-
await _maybe_await(on_tool_result, tr)
|
327
|
-
if request.verbose_output:
|
328
|
-
_pp_tool_result(tr, theme)
|
329
|
-
yield chunk
|
330
|
-
|
331
|
-
# ------------------------ RESULT -----------------------------------
|
332
|
-
elif typ == "result":
|
333
|
-
session.result = obj.get("result", "").strip()
|
334
|
-
session.usage = obj.get("usage", {})
|
335
|
-
session.total_cost_usd = obj.get("total_cost_usd")
|
336
|
-
session.num_turns = obj.get("num_turns")
|
337
|
-
session.duration_ms = obj.get("duration_ms")
|
338
|
-
session.duration_api_ms = obj.get("duration_api_ms")
|
339
|
-
session.is_error = obj.get("is_error", False)
|
340
|
-
|
341
|
-
# ------------------------ DONE -------------------------------------
|
342
|
-
elif typ == "done":
|
343
|
-
break
|
344
|
-
|
345
|
-
# final pretty print
|
346
|
-
await _maybe_await(on_final, session)
|
347
|
-
if request.verbose_output:
|
348
|
-
_pp_final(session, theme)
|
349
|
-
|
350
|
-
yield session
|
351
|
-
|
352
|
-
|
353
|
-
__all__ = (
|
354
|
-
"CLAUDE",
|
355
|
-
"stream_claude_code_cli",
|
356
|
-
"ndjson_from_cli",
|
357
|
-
"ClaudeChunk",
|
358
|
-
"ClaudeSession",
|
359
|
-
)
|