lionagi 0.16.2__py3-none-any.whl → 0.16.3__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.
Files changed (44) hide show
  1. lionagi/adapters/_utils.py +0 -14
  2. lionagi/ln/__init__.py +4 -0
  3. lionagi/ln/fuzzy/__init__.py +4 -1
  4. lionagi/ln/fuzzy/_fuzzy_validate.py +109 -0
  5. lionagi/ln/fuzzy/_to_dict.py +388 -0
  6. lionagi/models/__init__.py +0 -2
  7. lionagi/operations/communicate/communicate.py +1 -1
  8. lionagi/operations/parse/parse.py +1 -1
  9. lionagi/protocols/generic/pile.py +1 -1
  10. lionagi/protocols/operatives/operative.py +2 -2
  11. lionagi/service/connections/match_endpoint.py +2 -10
  12. lionagi/service/connections/providers/types.py +1 -3
  13. lionagi/service/hooks/hook_event.py +1 -1
  14. lionagi/service/hooks/hook_registry.py +1 -1
  15. lionagi/service/rate_limited_processor.py +1 -1
  16. lionagi/utils.py +3 -335
  17. lionagi/version.py +1 -1
  18. {lionagi-0.16.2.dist-info → lionagi-0.16.3.dist-info}/METADATA +3 -12
  19. {lionagi-0.16.2.dist-info → lionagi-0.16.3.dist-info}/RECORD +21 -43
  20. lionagi/adapters/postgres_model_adapter.py +0 -131
  21. lionagi/libs/concurrency.py +0 -1
  22. lionagi/libs/nested/__init__.py +0 -3
  23. lionagi/libs/nested/flatten.py +0 -172
  24. lionagi/libs/nested/nfilter.py +0 -59
  25. lionagi/libs/nested/nget.py +0 -45
  26. lionagi/libs/nested/ninsert.py +0 -104
  27. lionagi/libs/nested/nmerge.py +0 -158
  28. lionagi/libs/nested/npop.py +0 -69
  29. lionagi/libs/nested/nset.py +0 -94
  30. lionagi/libs/nested/unflatten.py +0 -83
  31. lionagi/libs/nested/utils.py +0 -189
  32. lionagi/libs/parse.py +0 -31
  33. lionagi/libs/schema/json_schema.py +0 -231
  34. lionagi/libs/unstructured/__init__.py +0 -0
  35. lionagi/libs/unstructured/pdf_to_image.py +0 -45
  36. lionagi/libs/unstructured/read_image_to_base64.py +0 -33
  37. lionagi/libs/validate/fuzzy_match_keys.py +0 -7
  38. lionagi/libs/validate/fuzzy_validate_mapping.py +0 -144
  39. lionagi/libs/validate/string_similarity.py +0 -7
  40. lionagi/libs/validate/xml_parser.py +0 -203
  41. lionagi/models/note.py +0 -387
  42. lionagi/service/connections/providers/claude_code_.py +0 -299
  43. {lionagi-0.16.2.dist-info → lionagi-0.16.3.dist-info}/WHEEL +0 -0
  44. {lionagi-0.16.2.dist-info → lionagi-0.16.3.dist-info}/licenses/LICENSE +0 -0
lionagi/models/note.py DELETED
@@ -1,387 +0,0 @@
1
- # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- from collections.abc import ItemsView, Iterator, ValuesView
6
- from typing import Any, TypeAlias
7
-
8
- from pydantic import BaseModel, ConfigDict, Field, field_serializer
9
- from typing_extensions import override
10
-
11
- from lionagi.libs.nested.flatten import flatten
12
- from lionagi.utils import UNDEFINED, copy, to_list
13
-
14
- IndiceType: TypeAlias = str | list[str | int]
15
-
16
-
17
- class Note(BaseModel):
18
- """Container for managing nested dictionary data structures.
19
-
20
- A flexible container that provides deep nested data access, dictionary-like
21
- interface, flattening capabilities, and update operations for managing
22
- complex nested data structures.
23
-
24
- Args:
25
- **kwargs: Key-value pairs for initial content.
26
-
27
- Attributes:
28
- content: Nested dictionary structure.
29
- model_config: Configuration allowing arbitrary types.
30
-
31
- Examples:
32
- >>> note = Note(
33
- ... user={
34
- ... "name": "John",
35
- ... "settings": {
36
- ... "theme": "dark"
37
- ... }
38
- ... }
39
- ... )
40
- >>>
41
- >>> # Access nested data
42
- >>> name = note.get(["user", "name"])
43
- >>> theme = note["user"]["settings"]["theme"]
44
- >>>
45
- >>> # Update nested structure
46
- >>> note.update(["user", "settings"], {"language": "en"})
47
- """
48
-
49
- content: dict[str, Any] = Field(
50
- default_factory=dict
51
- ) # Nested data structure
52
-
53
- model_config = ConfigDict(
54
- arbitrary_types_allowed=True,
55
- use_enum_values=True,
56
- populate_by_name=True,
57
- )
58
-
59
- def __init__(self, **kwargs: Any) -> None:
60
- """Initialize Note with dictionary data.
61
-
62
- Args:
63
- **kwargs: Key-value pairs that will form the initial nested
64
- dictionary structure.
65
- """
66
- super().__init__()
67
- self.content = kwargs
68
-
69
- @field_serializer("content")
70
- def _serialize_content(self, value: Any) -> dict[str, Any]:
71
- """Serialize content to dictionary format.
72
-
73
- Args:
74
- value: Content to serialize
75
-
76
- Returns:
77
- Deep copy of content dictionary
78
- """
79
- output_dict = copy(value, deep=True)
80
- return output_dict
81
-
82
- def to_dict(self) -> dict[str, Any]:
83
- """Convert Note to dictionary, excluding undefined values.
84
-
85
- Returns:
86
- Dictionary representation with UNDEFINED values removed
87
- """
88
- out = copy(self.content)
89
- for k, v in self.content.items():
90
- if v is UNDEFINED:
91
- out.pop(k)
92
- return out
93
-
94
- def pop(
95
- self,
96
- indices: IndiceType,
97
- /,
98
- default: Any = UNDEFINED,
99
- ) -> Any:
100
- """Remove and return item from nested structure.
101
-
102
- Removes and returns the value at the specified path in the nested
103
- structure. If the path doesn't exist and no default is provided,
104
- raises KeyError.
105
-
106
- Args:
107
- indices: Path to the item to remove, can be a string for top-level
108
- keys or a list for nested access.
109
- default: Value to return if the path is not found. If not provided
110
- and path is not found, raises KeyError.
111
-
112
- Returns:
113
- The value that was removed, or the default value if provided and
114
- path not found.
115
-
116
- Raises:
117
- KeyError: If the path is not found and no default value is provided.
118
- """
119
- from lionagi.libs.nested.npop import npop
120
-
121
- indices = to_list(indices, flatten=True, dropna=True)
122
- return npop(self.content, indices, default)
123
-
124
- def insert(self, indices: IndiceType, value: Any, /) -> None:
125
- """Insert value into nested structure at specified indices.
126
-
127
- Args:
128
- indices: Path where to insert
129
- value: Value to insert
130
- """
131
- from lionagi.libs.nested.ninsert import ninsert
132
-
133
- indices = to_list(indices, flatten=True, dropna=True)
134
- ninsert(self.content, indices, value)
135
-
136
- def set(self, indices: IndiceType, value: Any, /) -> None:
137
- """Set value in nested structure at specified indices.
138
-
139
- Args:
140
- indices: Path where to set
141
- value: Value to set
142
- """
143
- indices = to_list(indices, flatten=True, dropna=True)
144
- if self.get(indices, None) is None:
145
- self.insert(indices, value)
146
- else:
147
- from lionagi.libs.nested.nset import nset
148
-
149
- nset(self.content, indices, value)
150
-
151
- def get(
152
- self,
153
- indices: IndiceType,
154
- /,
155
- default: Any = UNDEFINED,
156
- ) -> Any:
157
- """Get value from nested structure at specified indices.
158
-
159
- Retrieves the value at the specified path in the nested structure.
160
- If the path doesn't exist and no default is provided, raises KeyError.
161
-
162
- Args:
163
- indices: Path to the value, can be a string for top-level keys
164
- or a list for nested access.
165
- default: Value to return if the path is not found. If not provided
166
- and path is not found, raises KeyError.
167
-
168
- Returns:
169
- The value at the specified path, or the default value if provided
170
- and path not found.
171
-
172
- Raises:
173
- KeyError: If the path is not found and no default value is provided.
174
- """
175
- from lionagi.libs.nested.nget import nget
176
-
177
- indices = to_list(indices, flatten=True, dropna=True)
178
- return nget(self.content, indices, default)
179
-
180
- def keys(self, /, flat: bool = False, **kwargs: Any) -> list:
181
- """Get keys of the Note.
182
-
183
- Args:
184
- flat: If True, return flattened keys
185
- kwargs: Additional flattening options
186
-
187
- Returns:
188
- List of keys, optionally flattened
189
- """
190
- if flat:
191
- kwargs["coerce_keys"] = kwargs.get("coerce_keys", False)
192
- kwargs["coerce_sequence"] = kwargs.get("coerce_sequence", "list")
193
- return flatten(self.content, **kwargs).keys()
194
- return list(self.content.keys())
195
-
196
- def values(self, /, flat: bool = False, **kwargs: Any) -> ValuesView:
197
- """Get values of the Note.
198
-
199
- Returns either a view of top-level values or, if flat=True, a view
200
- of all values in the flattened nested structure.
201
-
202
- Args:
203
- flat: If True, returns values from all levels of the nested
204
- structure. If False, returns only top-level values.
205
- kwargs: Additional options for flattening behavior when flat=True.
206
- Common options include coerce_keys and coerce_sequence.
207
-
208
- Returns:
209
- A ValuesView object containing either top-level values or all
210
- values from the flattened structure if flat=True.
211
- """
212
- if flat:
213
- kwargs["coerce_keys"] = kwargs.get("coerce_keys", False)
214
- kwargs["coerce_sequence"] = kwargs.get("coerce_sequence", "list")
215
- return flatten(self.content, **kwargs).values()
216
- return self.content.values()
217
-
218
- def items(self, /, flat: bool = False, **kwargs: Any) -> ItemsView:
219
- """Get items of the Note.
220
-
221
- Args:
222
- flat: If True, return flattened items
223
- kwargs: Additional flattening options
224
-
225
- Returns:
226
- View of items, optionally flattened
227
- """
228
- if flat:
229
- kwargs["coerce_keys"] = kwargs.get("coerce_keys", False)
230
- kwargs["coerce_sequence"] = kwargs.get("coerce_sequence", "list")
231
- return flatten(self.content, **kwargs).items()
232
- return self.content.items()
233
-
234
- def clear(self) -> None:
235
- """Clear all content."""
236
- self.content.clear()
237
-
238
- def update(
239
- self,
240
- indices: IndiceType,
241
- value: Any,
242
- ) -> None:
243
- """Update nested structure at specified indices.
244
-
245
- Updates the value at the specified path in the nested structure.
246
- The behavior depends on the existing value and the update value:
247
- - If path doesn't exist: creates it with value (wrapped in list if scalar)
248
- - If existing is list: extends with value if list, appends if scalar
249
- - If existing is dict: updates with value if dict, raises error if not
250
-
251
- Args:
252
- indices: Path to the location to update, can be a string for
253
- top-level keys or a list for nested access.
254
- value: The new value to set. Must be compatible with existing
255
- value type (dict for dict, any value for list).
256
-
257
- Raises:
258
- ValueError: If trying to update a dictionary with a non-dictionary
259
- value.
260
- """
261
- existing = None
262
- if not indices:
263
- existing = self.content
264
- else:
265
- existing = self.get(indices, None)
266
-
267
- if existing is None:
268
- if not isinstance(value, (list, dict)):
269
- value = [value]
270
- self.set(indices, value)
271
-
272
- if isinstance(existing, list):
273
- if isinstance(value, list):
274
- existing.extend(value)
275
- else:
276
- existing.append(value)
277
-
278
- elif isinstance(existing, dict):
279
- if isinstance(value, self.__class__):
280
- value = value.content
281
-
282
- if isinstance(value, dict):
283
- existing.update(value)
284
- else:
285
- raise ValueError(
286
- "Cannot update a dictionary with a non-dictionary value."
287
- )
288
-
289
- @classmethod
290
- def from_dict(cls, kwargs: Any) -> "Note":
291
- """Create Note instance from dictionary.
292
-
293
- Args:
294
- kwargs: Dictionary to initialize with
295
-
296
- Returns:
297
- New Note instance
298
- """
299
- return cls(**kwargs)
300
-
301
- def __contains__(self, indices: IndiceType) -> bool:
302
- """Check if indices exist in content.
303
-
304
- Implements the 'in' operator for checking path existence in the nested
305
- structure.
306
-
307
- Args:
308
- indices: Path to check, can be a string for top-level keys or a
309
- list for nested access.
310
-
311
- Returns:
312
- True if the path exists in the nested structure, False otherwise.
313
- """
314
- return self.content.get(indices, UNDEFINED) is not UNDEFINED
315
-
316
- def __len__(self) -> int:
317
- """Get length of content.
318
-
319
- Implements len() function to return the number of top-level keys in
320
- the nested structure.
321
-
322
- Returns:
323
- The number of top-level keys in the content dictionary.
324
- """
325
- return len(self.content)
326
-
327
- def __iter__(self) -> Iterator[str]:
328
- """Get iterator over content.
329
-
330
- Returns:
331
- Iterator over top-level keys
332
- """
333
- return iter(self.content)
334
-
335
- def __next__(self) -> str:
336
- """Get next item from content iterator.
337
-
338
- Returns:
339
- Next key in iteration
340
- """
341
- return next(iter(self.content))
342
-
343
- @override
344
- def __str__(self) -> str:
345
- """Get string representation of content.
346
-
347
- Implements str() function to provide a simple string representation
348
- of the Note's content. Uses the standard dictionary string format.
349
-
350
- Returns:
351
- A string representation of the content dictionary, showing its
352
- current state.
353
- """
354
- return str(self.content)
355
-
356
- @override
357
- def __repr__(self) -> str:
358
- """Get detailed string representation of content.
359
-
360
- Returns:
361
- Detailed string representation of content dict
362
- """
363
- return repr(self.content)
364
-
365
- def __getitem__(self, indices: IndiceType) -> Any:
366
- """Get item using index notation.
367
-
368
- Args:
369
- indices: Path to value
370
-
371
- Returns:
372
- Value at path
373
-
374
- Raises:
375
- KeyError: If path not found
376
- """
377
- indices = to_list(indices, flatten=True, dropna=True)
378
- return self.get(indices)
379
-
380
- def __setitem__(self, indices: IndiceType, value: Any) -> None:
381
- """Set item using index notation.
382
-
383
- Args:
384
- indices: Path where to set
385
- value: Value to set
386
- """
387
- self.set(indices, value)
@@ -1,299 +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 warnings
8
-
9
- from pydantic import BaseModel
10
-
11
- from lionagi import ln
12
- from lionagi.libs.schema.as_readable import as_readable
13
- from lionagi.service.connections.endpoint import Endpoint
14
- from lionagi.service.connections.endpoint_config import EndpointConfig
15
- from lionagi.utils import to_dict, to_list
16
-
17
- from ...third_party.claude_code import (
18
- CLAUDE_CODE_OPTION_PARAMS,
19
- HAS_CLAUDE_CODE_SDK,
20
- ClaudeCodeRequest,
21
- ClaudePermission,
22
- stream_cc_sdk_events,
23
- )
24
-
25
- __all__ = (
26
- "ClaudeCodeRequest",
27
- "CLAUDE_CODE_OPTION_PARAMS", # backward compatibility
28
- "ClaudePermission", # backward compatibility
29
- "ClaudeCodeEndpoint",
30
- )
31
-
32
-
33
- # --------------------------------------------------------------------------- SDK endpoint
34
-
35
- _get_config = lambda: EndpointConfig(
36
- name="claude_code",
37
- provider="claude_code",
38
- base_url="internal",
39
- endpoint="query",
40
- request_options=ClaudeCodeRequest,
41
- timeout=3000,
42
- api_key="dummy-key",
43
- )
44
-
45
-
46
- ENDPOINT_CONFIG = _get_config() # backward compatibility
47
-
48
-
49
- class ClaudeCodeEndpoint(Endpoint):
50
- """Direct Python-SDK (non-CLI) endpoint - unchanged except for bug-fixes."""
51
-
52
- def __init__(self, config: EndpointConfig = None, **kwargs):
53
- if not HAS_CLAUDE_CODE_SDK:
54
- raise ImportError(
55
- "claude_code_sdk is not installed. "
56
- "Please install it with `uv pip install lionagi[claude_code_sdk]`."
57
- )
58
- warnings.warn(
59
- "The claude_code `query` endpoint is deprecated. Use `query_cli` endpoint instead.",
60
- DeprecationWarning,
61
- )
62
-
63
- config = config or _get_config()
64
- super().__init__(config=config, **kwargs)
65
-
66
- def create_payload(self, request: dict | BaseModel, **kwargs):
67
- req_dict = {**self.config.kwargs, **to_dict(request), **kwargs}
68
- messages = req_dict.pop("messages")
69
- req_obj = ClaudeCodeRequest.create(messages=messages, **req_dict)
70
- return {"request": req_obj}, {}
71
-
72
- async def stream(self, request: dict | BaseModel, **kwargs):
73
- payload, _ = self.create_payload(request, **kwargs)
74
- async for chunk in stream_cc_sdk_events(payload["request"]):
75
- yield chunk
76
-
77
- def _parse_claude_code_response(self, responses: list) -> dict:
78
- """Parse Claude Code responses into a clean chat completions-like format.
79
-
80
- Claude Code returns a list of messages:
81
- - SystemMessage: initialization info
82
- - AssistantMessage(s): actual assistant responses with content blocks
83
- - UserMessage(s): for tool use interactions
84
- - ResultMessage: final result with metadata
85
-
86
- When Claude Code uses tools, the ResultMessage.result may be None.
87
- In that case, we need to look at the tool results in UserMessages.
88
- """
89
- results = {
90
- "session_id": None,
91
- "model": "claude-code",
92
- "result": "",
93
- "tool_uses": [],
94
- "tool_results": [],
95
- "is_error": False,
96
- "num_turns": None,
97
- "total_cost_usd": None,
98
- "usage": {
99
- "prompt_tokens": 0,
100
- "completion_tokens": 0,
101
- "total_tokens": 0,
102
- },
103
- }
104
- from claude_code_sdk import types as cc_types
105
-
106
- for response in responses:
107
- if isinstance(response, cc_types.SystemMessage):
108
- results["session_id"] = response.data.get("session_id")
109
- results["model"] = response.data.get("model", "claude-code")
110
- if isinstance(
111
- response, cc_types.AssistantMessage | cc_types.UserMessage
112
- ):
113
- for block in to_list(
114
- response.content,
115
- flatten=True,
116
- flatten_tuple_set=True,
117
- dropna=True,
118
- ):
119
- if isinstance(block, cc_types.TextBlock):
120
- results["result"] += block.text.strip() + "\n"
121
-
122
- if isinstance(block, cc_types.ToolUseBlock):
123
- entry = {
124
- "id": block.id,
125
- "name": block.name,
126
- "input": block.input,
127
- }
128
- results["tool_uses"].append(entry)
129
-
130
- if isinstance(block, cc_types.ToolResultBlock):
131
- results["tool_results"].append(
132
- {
133
- "tool_use_id": block.tool_use_id,
134
- "content": block.content,
135
- "is_error": block.is_error,
136
- }
137
- )
138
-
139
- if isinstance(response, cc_types.ResultMessage):
140
- if response.result:
141
- results["result"] = str(response.result).strip()
142
- results["usage"] = response.usage
143
- results["is_error"] = response.is_error
144
- results["total_cost_usd"] = response.total_cost_usd
145
- results["num_turns"] = response.num_turns
146
- results["duration_ms"] = response.duration_ms
147
- results["duration_api_ms"] = response.duration_api_ms
148
-
149
- return results
150
-
151
- async def _call(
152
- self,
153
- payload: dict,
154
- headers: dict,
155
- **kwargs,
156
- ):
157
- from claude_code_sdk import query as sdk_query
158
- from claude_code_sdk import types as cc_types
159
-
160
- responses = []
161
- request: ClaudeCodeRequest = payload["request"]
162
- system: cc_types.SystemMessage = None
163
-
164
- # 1. stream the Claude Code response
165
- async for chunk in self._stream_claude_code(**payload):
166
- if request.verbose_output:
167
- _display_message(chunk, theme=request.cli_display_theme)
168
-
169
- if isinstance(chunk, cc_types.SystemMessage):
170
- system = chunk
171
- responses.append(chunk)
172
-
173
- # 2. If the last response is not a ResultMessage and auto_finish is True,
174
- # we need to query Claude Code again to get the final result message.
175
- if request.auto_finish and not isinstance(
176
- responses[-1], cc_types.ResultMessage
177
- ):
178
- options = request.as_claude_options()
179
- options.continue_conversation = True
180
- options.max_turns = 1
181
- if system:
182
- options.resume = (
183
- system.data.get("session_id", None) if system else None
184
- )
185
-
186
- async for chunk in sdk_query(
187
- prompt="Please provide a the final result message only",
188
- options=options,
189
- ):
190
- if isinstance(chunk, cc_types.ResultMessage):
191
- if request.verbose_output:
192
- str_ = _verbose_output(chunk)
193
- if str_:
194
- as_readable(
195
- str_,
196
- md=True,
197
- display_str=True,
198
- format_curly=True,
199
- max_panel_width=100,
200
- theme=request.cli_display_theme,
201
- )
202
-
203
- responses.append(chunk)
204
-
205
-
206
- def _display_message(chunk, theme):
207
- from claude_code_sdk import types as cc_types
208
-
209
- if isinstance(
210
- chunk,
211
- cc_types.SystemMessage
212
- | cc_types.AssistantMessage
213
- | cc_types.UserMessage,
214
- ):
215
- str_ = _verbose_output(chunk)
216
- if str_:
217
- if str_.startswith("Claude:"):
218
- as_readable(
219
- str_,
220
- md=True,
221
- display_str=True,
222
- max_panel_width=100,
223
- theme=theme,
224
- )
225
- else:
226
- as_readable(
227
- str_,
228
- format_curly=True,
229
- display_str=True,
230
- max_panel_width=100,
231
- theme=theme,
232
- )
233
-
234
- if isinstance(chunk, cc_types.ResultMessage):
235
- str_ = _verbose_output(chunk)
236
- as_readable(
237
- str_,
238
- md=True,
239
- display_str=True,
240
- format_curly=True,
241
- max_panel_width=100,
242
- theme=theme,
243
- )
244
-
245
-
246
- def _verbose_output(res) -> str:
247
- from claude_code_sdk import types as cc_types
248
-
249
- str_ = ""
250
- if isinstance(res, cc_types.SystemMessage):
251
- str_ = f"Claude Code Session Started: {res.data.get('session_id', 'unknown')}"
252
- str_ += f"\nModel: {res.data.get('model', 'claude-code')}\n---"
253
- return str_
254
-
255
- if isinstance(res, cc_types.AssistantMessage | cc_types.UserMessage):
256
- for block in to_list(
257
- res.content, flatten=True, flatten_tuple_set=True, dropna=True
258
- ):
259
- if isinstance(block, cc_types.TextBlock):
260
- text = (
261
- block.text.strip() if isinstance(block.text, str) else ""
262
- )
263
- str_ += f"Claude:\n{text}"
264
-
265
- if isinstance(block, cc_types.ToolUseBlock):
266
- inp_ = None
267
-
268
- if isinstance(block.input, dict | list):
269
- inp_ = ln.json_dumps(
270
- block.input,
271
- pretty=True,
272
- sort_keys=True,
273
- append_newline=True,
274
- )
275
- else:
276
- inp_ = str(block.input)
277
-
278
- input = inp_[:200] + "..." if len(inp_) > 200 else inp_
279
- str_ += (
280
- f"Tool Use: {block.name} - {block.id}\n - Input: {input}"
281
- )
282
-
283
- if isinstance(block, cc_types.ToolResultBlock):
284
- content = str(block.content)
285
- content = (
286
- content[:200] + "..." if len(content) > 200 else content
287
- )
288
- str_ += (
289
- f"Tool Result: {block.tool_use_id}\n - Content: {content}"
290
- )
291
- return str_
292
-
293
- if isinstance(res, cc_types.ResultMessage):
294
- str_ += f"Session Completion - {res.session_id}"
295
- str_ += f"\nResult: {res.result or 'No result'}"
296
- str_ += f"\n- Cost: ${res.total_cost_usd:.4f} USD"
297
- str_ += f"\n- Duration: {res.duration_ms} ms (API: {res.duration_api_ms} ms)"
298
- str_ += f"\n- Turns: {res.num_turns}"
299
- return str_