lionagi 0.17.10__py3-none-any.whl → 0.18.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.
- lionagi/__init__.py +1 -2
- lionagi/_class_registry.py +1 -2
- lionagi/_errors.py +1 -2
- lionagi/adapters/async_postgres_adapter.py +2 -10
- lionagi/config.py +1 -2
- lionagi/fields/action.py +1 -2
- lionagi/fields/base.py +3 -0
- lionagi/fields/code.py +3 -0
- lionagi/fields/file.py +3 -0
- lionagi/fields/instruct.py +1 -2
- lionagi/fields/reason.py +1 -2
- lionagi/fields/research.py +3 -0
- lionagi/libs/__init__.py +1 -2
- lionagi/libs/file/__init__.py +1 -2
- lionagi/libs/file/chunk.py +1 -2
- lionagi/libs/file/process.py +1 -2
- lionagi/libs/schema/__init__.py +1 -2
- lionagi/libs/schema/as_readable.py +1 -2
- lionagi/libs/schema/extract_code_block.py +1 -2
- lionagi/libs/schema/extract_docstring.py +1 -2
- lionagi/libs/schema/function_to_schema.py +1 -2
- lionagi/libs/schema/load_pydantic_model_from_schema.py +1 -2
- lionagi/libs/schema/minimal_yaml.py +98 -0
- lionagi/libs/validate/__init__.py +1 -2
- lionagi/libs/validate/common_field_validators.py +1 -2
- lionagi/libs/validate/validate_boolean.py +1 -2
- lionagi/ln/fuzzy/_string_similarity.py +1 -2
- lionagi/ln/types.py +32 -5
- lionagi/models/__init__.py +1 -2
- lionagi/models/field_model.py +9 -1
- lionagi/models/hashable_model.py +4 -2
- lionagi/models/model_params.py +1 -2
- lionagi/models/operable_model.py +1 -2
- lionagi/models/schema_model.py +1 -2
- lionagi/operations/ReAct/ReAct.py +475 -239
- lionagi/operations/ReAct/__init__.py +1 -2
- lionagi/operations/ReAct/utils.py +4 -2
- lionagi/operations/__init__.py +1 -2
- lionagi/operations/act/__init__.py +2 -0
- lionagi/operations/act/act.py +206 -0
- lionagi/operations/brainstorm/__init__.py +1 -2
- lionagi/operations/brainstorm/brainstorm.py +1 -2
- lionagi/operations/brainstorm/prompt.py +1 -2
- lionagi/operations/builder.py +1 -2
- lionagi/operations/chat/__init__.py +1 -2
- lionagi/operations/chat/chat.py +131 -116
- lionagi/operations/communicate/communicate.py +102 -44
- lionagi/operations/flow.py +5 -6
- lionagi/operations/instruct/__init__.py +1 -2
- lionagi/operations/instruct/instruct.py +1 -2
- lionagi/operations/interpret/__init__.py +1 -2
- lionagi/operations/interpret/interpret.py +66 -22
- lionagi/operations/operate/__init__.py +1 -2
- lionagi/operations/operate/operate.py +213 -108
- lionagi/operations/parse/__init__.py +1 -2
- lionagi/operations/parse/parse.py +171 -144
- lionagi/operations/plan/__init__.py +1 -2
- lionagi/operations/plan/plan.py +1 -2
- lionagi/operations/plan/prompt.py +1 -2
- lionagi/operations/select/__init__.py +1 -2
- lionagi/operations/select/select.py +79 -19
- lionagi/operations/select/utils.py +2 -3
- lionagi/operations/types.py +120 -25
- lionagi/operations/utils.py +1 -2
- lionagi/protocols/__init__.py +1 -2
- lionagi/protocols/_concepts.py +1 -2
- lionagi/protocols/action/__init__.py +1 -2
- lionagi/protocols/action/function_calling.py +3 -20
- lionagi/protocols/action/manager.py +34 -4
- lionagi/protocols/action/tool.py +1 -2
- lionagi/protocols/contracts.py +1 -2
- lionagi/protocols/forms/__init__.py +1 -2
- lionagi/protocols/forms/base.py +1 -2
- lionagi/protocols/forms/flow.py +1 -2
- lionagi/protocols/forms/form.py +1 -2
- lionagi/protocols/forms/report.py +1 -2
- lionagi/protocols/generic/__init__.py +1 -2
- lionagi/protocols/generic/element.py +17 -65
- lionagi/protocols/generic/event.py +1 -2
- lionagi/protocols/generic/log.py +17 -14
- lionagi/protocols/generic/pile.py +3 -4
- lionagi/protocols/generic/processor.py +1 -2
- lionagi/protocols/generic/progression.py +1 -2
- lionagi/protocols/graph/__init__.py +1 -2
- lionagi/protocols/graph/edge.py +1 -2
- lionagi/protocols/graph/graph.py +1 -2
- lionagi/protocols/graph/node.py +1 -2
- lionagi/protocols/ids.py +1 -2
- lionagi/protocols/mail/__init__.py +1 -2
- lionagi/protocols/mail/exchange.py +1 -2
- lionagi/protocols/mail/mail.py +1 -2
- lionagi/protocols/mail/mailbox.py +1 -2
- lionagi/protocols/mail/manager.py +1 -2
- lionagi/protocols/mail/package.py +1 -2
- lionagi/protocols/messages/__init__.py +28 -2
- lionagi/protocols/messages/action_request.py +87 -186
- lionagi/protocols/messages/action_response.py +74 -133
- lionagi/protocols/messages/assistant_response.py +131 -161
- lionagi/protocols/messages/base.py +27 -20
- lionagi/protocols/messages/instruction.py +281 -626
- lionagi/protocols/messages/manager.py +113 -64
- lionagi/protocols/messages/message.py +88 -199
- lionagi/protocols/messages/system.py +53 -125
- lionagi/protocols/operatives/__init__.py +1 -2
- lionagi/protocols/operatives/operative.py +1 -2
- lionagi/protocols/operatives/step.py +1 -2
- lionagi/protocols/types.py +1 -4
- lionagi/service/connections/__init__.py +1 -2
- lionagi/service/connections/api_calling.py +1 -2
- lionagi/service/connections/endpoint.py +1 -10
- lionagi/service/connections/endpoint_config.py +1 -2
- lionagi/service/connections/header_factory.py +1 -2
- lionagi/service/connections/match_endpoint.py +1 -2
- lionagi/service/connections/mcp/__init__.py +1 -2
- lionagi/service/connections/mcp/wrapper.py +1 -2
- lionagi/service/connections/providers/__init__.py +1 -2
- lionagi/service/connections/providers/anthropic_.py +1 -2
- lionagi/service/connections/providers/claude_code_cli.py +1 -2
- lionagi/service/connections/providers/exa_.py +1 -2
- lionagi/service/connections/providers/nvidia_nim_.py +2 -27
- lionagi/service/connections/providers/oai_.py +30 -96
- lionagi/service/connections/providers/ollama_.py +4 -4
- lionagi/service/connections/providers/perplexity_.py +1 -2
- lionagi/service/hooks/__init__.py +1 -1
- lionagi/service/hooks/_types.py +1 -1
- lionagi/service/hooks/_utils.py +1 -1
- lionagi/service/hooks/hook_event.py +1 -1
- lionagi/service/hooks/hook_registry.py +1 -1
- lionagi/service/hooks/hooked_event.py +3 -4
- lionagi/service/imodel.py +1 -2
- lionagi/service/manager.py +1 -2
- lionagi/service/rate_limited_processor.py +1 -2
- lionagi/service/resilience.py +1 -2
- lionagi/service/third_party/anthropic_models.py +1 -2
- lionagi/service/third_party/claude_code.py +4 -4
- lionagi/service/third_party/openai_models.py +433 -0
- lionagi/service/token_calculator.py +1 -2
- lionagi/session/__init__.py +1 -2
- lionagi/session/branch.py +171 -180
- lionagi/session/session.py +4 -11
- lionagi/tools/__init__.py +1 -2
- lionagi/tools/base.py +1 -2
- lionagi/tools/file/__init__.py +1 -2
- lionagi/tools/file/reader.py +3 -4
- lionagi/tools/types.py +1 -2
- lionagi/utils.py +1 -2
- lionagi/version.py +1 -1
- {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/METADATA +1 -2
- lionagi-0.18.0.dist-info/RECORD +191 -0
- lionagi/operations/_act/__init__.py +0 -3
- lionagi/operations/_act/act.py +0 -87
- lionagi/protocols/messages/templates/README.md +0 -28
- lionagi/protocols/messages/templates/action_request.jinja2 +0 -5
- lionagi/protocols/messages/templates/action_response.jinja2 +0 -9
- lionagi/protocols/messages/templates/assistant_response.jinja2 +0 -6
- lionagi/protocols/messages/templates/instruction_message.jinja2 +0 -61
- lionagi/protocols/messages/templates/system_message.jinja2 +0 -11
- lionagi/protocols/messages/templates/tool_schemas.jinja2 +0 -7
- lionagi/service/connections/providers/types.py +0 -28
- lionagi/service/third_party/openai_model_names.py +0 -198
- lionagi/service/types.py +0 -59
- lionagi-0.17.10.dist-info/RECORD +0 -199
- {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/WHEEL +0 -0
- {lionagi-0.17.10.dist-info → lionagi-0.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,8 @@
|
|
1
|
-
# Copyright (c) 2023
|
2
|
-
#
|
1
|
+
# Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
|
3
2
|
# SPDX-License-Identifier: Apache-2.0
|
4
3
|
|
5
4
|
from typing import Any, Literal
|
6
5
|
|
7
|
-
from jinja2 import Template
|
8
6
|
from pydantic import BaseModel, JsonValue
|
9
7
|
|
10
8
|
from .._concepts import Manager
|
@@ -89,6 +87,7 @@ class MessageManager(Manager):
|
|
89
87
|
*,
|
90
88
|
instruction: JsonValue = None,
|
91
89
|
context: JsonValue = None,
|
90
|
+
handle_context: Literal["extend", "replace"] = "extend",
|
92
91
|
guidance: JsonValue = None,
|
93
92
|
images: list = None,
|
94
93
|
request_fields: JsonValue = None,
|
@@ -106,17 +105,49 @@ class MessageManager(Manager):
|
|
106
105
|
If `instruction` is an existing Instruction, it is updated in place.
|
107
106
|
Otherwise, a new instance is created.
|
108
107
|
"""
|
109
|
-
|
108
|
+
raw_params = {
|
110
109
|
k: v
|
111
110
|
for k, v in locals().items()
|
112
111
|
if k != "instruction" and v is not None
|
113
112
|
}
|
114
113
|
|
114
|
+
handle_ctx = raw_params.get("handle_context", "extend")
|
115
|
+
|
115
116
|
if isinstance(instruction, Instruction):
|
117
|
+
params = {
|
118
|
+
k: v for k, v in raw_params.items() if k != "handle_context"
|
119
|
+
}
|
120
|
+
ctx_value = params.pop("context", None)
|
121
|
+
if ctx_value is not None:
|
122
|
+
if isinstance(ctx_value, list):
|
123
|
+
ctx_list = list(ctx_value)
|
124
|
+
else:
|
125
|
+
ctx_list = [ctx_value]
|
126
|
+
if handle_ctx == "extend":
|
127
|
+
merged = list(instruction.content.prompt_context)
|
128
|
+
merged.extend(ctx_list)
|
129
|
+
params["context"] = merged
|
130
|
+
else:
|
131
|
+
params["context"] = list(ctx_list)
|
132
|
+
# Always replace in from_dict since we've already done the merge logic
|
133
|
+
params["handle_context"] = "replace"
|
116
134
|
instruction.update(**params)
|
117
135
|
return instruction
|
118
136
|
else:
|
119
|
-
|
137
|
+
# Build content dict for Instruction
|
138
|
+
content_dict = {
|
139
|
+
k: v
|
140
|
+
for k, v in raw_params.items()
|
141
|
+
if k not in ["sender", "recipient"]
|
142
|
+
}
|
143
|
+
content_dict["handle_context"] = handle_ctx
|
144
|
+
if instruction is not None:
|
145
|
+
content_dict["instruction"] = instruction
|
146
|
+
return Instruction(
|
147
|
+
content=content_dict,
|
148
|
+
sender=raw_params.get("sender"),
|
149
|
+
recipient=raw_params.get("recipient"),
|
150
|
+
)
|
120
151
|
|
121
152
|
@staticmethod
|
122
153
|
def create_assistant_response(
|
@@ -124,8 +155,6 @@ class MessageManager(Manager):
|
|
124
155
|
sender: Any = None,
|
125
156
|
recipient: Any = None,
|
126
157
|
assistant_response: AssistantResponse | Any = None,
|
127
|
-
template: Template | str = None,
|
128
|
-
template_context: dict[str, Any] = None,
|
129
158
|
) -> AssistantResponse:
|
130
159
|
"""
|
131
160
|
Build or update an `AssistantResponse`. If `assistant_response` is an
|
@@ -134,17 +163,23 @@ class MessageManager(Manager):
|
|
134
163
|
params = {
|
135
164
|
k: v
|
136
165
|
for k, v in locals().items()
|
137
|
-
if k
|
166
|
+
if k != "assistant_response" and v is not None
|
138
167
|
}
|
139
|
-
t_ctx = template_context or {}
|
140
|
-
params.update(t_ctx)
|
141
168
|
|
142
169
|
if isinstance(assistant_response, AssistantResponse):
|
143
170
|
assistant_response.update(**params)
|
144
171
|
return assistant_response
|
145
172
|
|
146
|
-
|
147
|
-
|
173
|
+
# Create new AssistantResponse
|
174
|
+
content_dict = (
|
175
|
+
{"assistant_response": assistant_response}
|
176
|
+
if assistant_response
|
177
|
+
else {}
|
178
|
+
)
|
179
|
+
return AssistantResponse(
|
180
|
+
content=content_dict,
|
181
|
+
sender=params.get("sender"),
|
182
|
+
recipient=params.get("recipient"),
|
148
183
|
)
|
149
184
|
|
150
185
|
@staticmethod
|
@@ -155,8 +190,6 @@ class MessageManager(Manager):
|
|
155
190
|
function: str = None,
|
156
191
|
arguments: dict[str, Any] = None,
|
157
192
|
action_request: ActionRequest | None = None,
|
158
|
-
template: Template | str = None,
|
159
|
-
template_context: dict[str, Any] = None,
|
160
193
|
) -> ActionRequest:
|
161
194
|
"""
|
162
195
|
Build or update an ActionRequest.
|
@@ -167,25 +200,31 @@ class MessageManager(Manager):
|
|
167
200
|
function: Function name for the request.
|
168
201
|
arguments: Arguments for the function.
|
169
202
|
action_request: Possibly existing ActionRequest to update.
|
170
|
-
template: Optional jinja template.
|
171
|
-
template_context: Extra context for the template.
|
172
203
|
|
173
204
|
Returns:
|
174
205
|
ActionRequest: The new or updated request object.
|
175
206
|
"""
|
176
207
|
params = {
|
177
|
-
|
178
|
-
|
179
|
-
"
|
180
|
-
"arguments": arguments,
|
181
|
-
"template": template,
|
208
|
+
k: v
|
209
|
+
for k, v in locals().items()
|
210
|
+
if k != "action_request" and v is not None
|
182
211
|
}
|
183
|
-
params.update(template_context or {})
|
184
212
|
|
185
213
|
if isinstance(action_request, ActionRequest):
|
186
214
|
action_request.update(**params)
|
187
215
|
return action_request
|
188
|
-
|
216
|
+
|
217
|
+
# Create new ActionRequest
|
218
|
+
content_dict = {}
|
219
|
+
if function:
|
220
|
+
content_dict["function"] = function
|
221
|
+
if arguments:
|
222
|
+
content_dict["arguments"] = arguments
|
223
|
+
return ActionRequest(
|
224
|
+
content=content_dict,
|
225
|
+
sender=params.get("sender"),
|
226
|
+
recipient=params.get("recipient"),
|
227
|
+
)
|
189
228
|
|
190
229
|
@staticmethod
|
191
230
|
def create_action_response(
|
@@ -218,17 +257,27 @@ class MessageManager(Manager):
|
|
218
257
|
raise ValueError(
|
219
258
|
"Error: please provide a corresponding action request for an action response."
|
220
259
|
)
|
221
|
-
params = {
|
222
|
-
"action_request": action_request,
|
223
|
-
"output": action_output,
|
224
|
-
"sender": sender,
|
225
|
-
"recipient": recipient,
|
226
|
-
}
|
227
260
|
if isinstance(action_response, ActionResponse):
|
228
|
-
action_response.update(
|
261
|
+
action_response.update(
|
262
|
+
output=action_output, sender=sender, recipient=recipient
|
263
|
+
)
|
229
264
|
return action_response
|
230
265
|
|
231
|
-
|
266
|
+
# Create new ActionResponse
|
267
|
+
content_dict = {
|
268
|
+
"function": action_request.content.function,
|
269
|
+
"arguments": action_request.content.arguments,
|
270
|
+
"output": action_output,
|
271
|
+
"action_request_id": str(action_request.id),
|
272
|
+
}
|
273
|
+
response = ActionResponse(
|
274
|
+
content=content_dict, sender=sender, recipient=recipient
|
275
|
+
)
|
276
|
+
|
277
|
+
# Update the request to reference this response
|
278
|
+
action_request.content.action_response_id = str(response.id)
|
279
|
+
|
280
|
+
return response
|
232
281
|
|
233
282
|
@staticmethod
|
234
283
|
def create_system(
|
@@ -237,28 +286,33 @@ class MessageManager(Manager):
|
|
237
286
|
system_datetime: bool | str = None,
|
238
287
|
sender: Any = None,
|
239
288
|
recipient: Any = None,
|
240
|
-
template: Template | str = None,
|
241
|
-
template_context: dict[str, Any] = None,
|
242
289
|
) -> System:
|
243
290
|
"""
|
244
291
|
Create or update a `System` message. If `system` is an instance, update.
|
245
292
|
Otherwise, create a new System message.
|
246
293
|
"""
|
247
294
|
params = {
|
248
|
-
|
249
|
-
|
250
|
-
"
|
251
|
-
"template": template,
|
252
|
-
**(template_context or {}),
|
295
|
+
k: v
|
296
|
+
for k, v in locals().items()
|
297
|
+
if k != "system" and v is not None
|
253
298
|
}
|
299
|
+
|
254
300
|
if isinstance(system, System):
|
255
301
|
system.update(**params)
|
256
302
|
return system
|
257
303
|
|
304
|
+
# Create new System message
|
305
|
+
content_dict = {}
|
258
306
|
if system:
|
259
|
-
|
260
|
-
|
261
|
-
|
307
|
+
content_dict["system_message"] = system
|
308
|
+
if system_datetime is not None:
|
309
|
+
content_dict["system_datetime"] = system_datetime
|
310
|
+
|
311
|
+
return System(
|
312
|
+
content=content_dict if content_dict else None,
|
313
|
+
sender=params.get("sender"),
|
314
|
+
recipient=params.get("recipient"),
|
315
|
+
)
|
262
316
|
|
263
317
|
def add_message(
|
264
318
|
self,
|
@@ -266,12 +320,11 @@ class MessageManager(Manager):
|
|
266
320
|
# common
|
267
321
|
sender: SenderRecipient = None,
|
268
322
|
recipient: SenderRecipient = None,
|
269
|
-
template: Template | str = None,
|
270
|
-
template_context: dict[str, Any] = None,
|
271
323
|
metadata: dict[str, Any] = None,
|
272
324
|
# instruction
|
273
325
|
instruction: JsonValue = None,
|
274
326
|
context: JsonValue = None,
|
327
|
+
handle_context: Literal["extend", "replace"] = "extend",
|
275
328
|
guidance: JsonValue = None,
|
276
329
|
request_fields: JsonValue = None,
|
277
330
|
plain_content: JsonValue = None,
|
@@ -300,18 +353,13 @@ class MessageManager(Manager):
|
|
300
353
|
- ActionRequest / ActionResponse
|
301
354
|
"""
|
302
355
|
_msg = None
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
action_request,
|
311
|
-
)
|
312
|
-
)
|
313
|
-
> 1
|
314
|
-
):
|
356
|
+
# When creating ActionResponse, both action_request and action_output are needed
|
357
|
+
# So don't count action_request as a message type when action_output is present
|
358
|
+
message_types = [instruction, assistant_response, system]
|
359
|
+
if action_request and not action_output:
|
360
|
+
message_types.append(action_request)
|
361
|
+
|
362
|
+
if sum(bool(x) for x in message_types) > 1:
|
315
363
|
raise ValueError("Only one message type can be added at a time.")
|
316
364
|
|
317
365
|
if system:
|
@@ -320,8 +368,6 @@ class MessageManager(Manager):
|
|
320
368
|
system_datetime=system_datetime,
|
321
369
|
sender=sender,
|
322
370
|
recipient=recipient,
|
323
|
-
template=template,
|
324
|
-
template_context=template_context,
|
325
371
|
)
|
326
372
|
self.set_system(_msg)
|
327
373
|
|
@@ -334,15 +380,15 @@ class MessageManager(Manager):
|
|
334
380
|
recipient=recipient,
|
335
381
|
)
|
336
382
|
|
337
|
-
elif action_request or (
|
383
|
+
elif action_request or (
|
384
|
+
action_function and action_arguments is not None
|
385
|
+
):
|
338
386
|
_msg = self.create_action_request(
|
339
387
|
sender=sender,
|
340
388
|
recipient=recipient,
|
341
389
|
function=action_function,
|
342
390
|
arguments=action_arguments,
|
343
391
|
action_request=action_request,
|
344
|
-
template=template,
|
345
|
-
template_context=template_context,
|
346
392
|
)
|
347
393
|
|
348
394
|
elif assistant_response:
|
@@ -350,14 +396,13 @@ class MessageManager(Manager):
|
|
350
396
|
sender=sender,
|
351
397
|
recipient=recipient,
|
352
398
|
assistant_response=assistant_response,
|
353
|
-
template=template,
|
354
|
-
template_context=template_context,
|
355
399
|
)
|
356
400
|
|
357
401
|
else:
|
358
402
|
_msg = self.create_instruction(
|
359
403
|
instruction=instruction,
|
360
404
|
context=context,
|
405
|
+
handle_context=handle_context,
|
361
406
|
guidance=guidance,
|
362
407
|
images=images,
|
363
408
|
request_fields=request_fields,
|
@@ -467,7 +512,9 @@ class MessageManager(Manager):
|
|
467
512
|
Convenience method to strip 'tool_schemas' from the most recent Instruction.
|
468
513
|
"""
|
469
514
|
if self.last_instruction:
|
470
|
-
self.messages[
|
515
|
+
self.messages[
|
516
|
+
self.last_instruction.id
|
517
|
+
].content.tool_schemas.clear()
|
471
518
|
|
472
519
|
def concat_recent_action_responses_to_instruction(
|
473
520
|
self, instruction: Instruction
|
@@ -478,7 +525,9 @@ class MessageManager(Manager):
|
|
478
525
|
"""
|
479
526
|
for i in reversed(list(self.messages.progression)):
|
480
527
|
if isinstance(self.messages[i], ActionResponse):
|
481
|
-
instruction.
|
528
|
+
instruction.content.prompt_context.append(
|
529
|
+
self.messages[i].content
|
530
|
+
)
|
482
531
|
else:
|
483
532
|
break
|
484
533
|
|
@@ -1,253 +1,142 @@
|
|
1
|
-
# Copyright (c) 2023
|
2
|
-
#
|
1
|
+
# Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
|
3
2
|
# SPDX-License-Identifier: Apache-2.0
|
4
3
|
|
5
|
-
from
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import Any, ClassVar
|
6
6
|
|
7
|
-
from
|
8
|
-
from typing import Any
|
7
|
+
from pydantic import field_serializer, field_validator
|
9
8
|
|
10
|
-
from
|
11
|
-
from pydantic import Field, PrivateAttr, field_serializer
|
12
|
-
|
13
|
-
from lionagi import ln
|
9
|
+
from lionagi.ln.types import DataClass
|
14
10
|
|
15
11
|
from .._concepts import Sendable
|
16
|
-
from ..generic.element import Element, IDType
|
17
|
-
from ..generic.log import Log
|
18
12
|
from ..graph.node import Node
|
19
13
|
from .base import (
|
20
|
-
MessageFlag,
|
21
14
|
MessageRole,
|
22
15
|
SenderRecipient,
|
16
|
+
serialize_sender_recipient,
|
23
17
|
validate_sender_recipient,
|
24
18
|
)
|
25
19
|
|
26
|
-
template_path = Path(__file__).parent / "templates"
|
27
|
-
jinja_env = Environment(loader=FileSystemLoader(template_path))
|
28
|
-
|
29
|
-
__all__ = ("RoledMessage",)
|
30
20
|
|
21
|
+
@dataclass(slots=True)
|
22
|
+
class MessageContent(DataClass):
|
23
|
+
"""A base class for message content structures."""
|
31
24
|
|
32
|
-
|
33
|
-
"""
|
34
|
-
A base class for all messages that have a `role` and carry structured
|
35
|
-
`content`. Subclasses might be `Instruction`, `ActionRequest`, etc.
|
36
|
-
"""
|
25
|
+
_none_as_sentinel: ClassVar[bool] = True
|
37
26
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
27
|
+
@property
|
28
|
+
def rendered(self) -> str:
|
29
|
+
"""Render the content as a string."""
|
30
|
+
raise NotImplementedError(
|
31
|
+
"Subclasses must implement rendered property."
|
32
|
+
)
|
42
33
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
34
|
+
@classmethod
|
35
|
+
def from_dict(cls, data: dict[str, Any]) -> "MessageContent":
|
36
|
+
"""Create an instance from a dictionary."""
|
37
|
+
raise NotImplementedError(
|
38
|
+
"Subclasses must implement from_dict method."
|
39
|
+
)
|
47
40
|
|
48
|
-
_flag: MessageFlag | None = PrivateAttr(None)
|
49
41
|
|
50
|
-
|
42
|
+
class RoledMessage(Node, Sendable):
|
43
|
+
"""Base class for all messages with a role and structured content.
|
51
44
|
|
52
|
-
|
53
|
-
|
54
|
-
title="Sender",
|
55
|
-
description="The ID of the sender node or a role.",
|
56
|
-
)
|
45
|
+
Subclasses must provide a concrete MessageContent type.
|
46
|
+
"""
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
)
|
48
|
+
role: MessageRole = MessageRole.UNSET
|
49
|
+
content: MessageContent
|
50
|
+
sender: SenderRecipient | None = MessageRole.UNSET
|
51
|
+
recipient: SenderRecipient | None = MessageRole.UNSET
|
63
52
|
|
64
53
|
@field_serializer("sender", "recipient")
|
65
54
|
def _serialize_sender_recipient(self, value: SenderRecipient) -> str:
|
66
|
-
|
67
|
-
return value.value
|
68
|
-
if isinstance(value, str):
|
69
|
-
return value
|
70
|
-
if isinstance(value, Element):
|
71
|
-
return str(value.id)
|
72
|
-
if isinstance(value, IDType):
|
73
|
-
return str(value)
|
74
|
-
return str(value)
|
55
|
+
return serialize_sender_recipient(value)
|
75
56
|
|
76
|
-
@
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
Returns:
|
83
|
-
list[dict[str,Any]] | None: If no images found, None.
|
84
|
-
"""
|
85
|
-
msg_ = self.chat_msg
|
86
|
-
if isinstance(msg_, dict) and isinstance(msg_["content"], list):
|
87
|
-
return [i for i in msg_["content"] if i["type"] == "image_url"]
|
88
|
-
return None
|
57
|
+
@field_validator("sender", "recipient")
|
58
|
+
def _validate_sender_recipient(cls, v):
|
59
|
+
if v is None:
|
60
|
+
return None
|
61
|
+
return validate_sender_recipient(v)
|
89
62
|
|
90
63
|
@property
|
91
64
|
def chat_msg(self) -> dict[str, Any] | None:
|
92
|
-
"""
|
93
|
-
A dictionary representation typically used in chat-based contexts.
|
94
|
-
|
95
|
-
Returns:
|
96
|
-
dict: `{"role": <role>, "content": <rendered content>}`
|
97
|
-
"""
|
65
|
+
"""A dictionary representation typically used in chat-based contexts."""
|
98
66
|
try:
|
99
|
-
|
67
|
+
role_str = (
|
68
|
+
self.role.value
|
69
|
+
if isinstance(self.role, MessageRole)
|
70
|
+
else str(self.role)
|
71
|
+
)
|
72
|
+
return {"role": role_str, "content": self.rendered}
|
100
73
|
except Exception:
|
101
74
|
return None
|
102
75
|
|
103
76
|
@property
|
104
77
|
def rendered(self) -> str:
|
105
|
-
"""
|
106
|
-
Attempt to format the message with a Jinja template (if provided).
|
107
|
-
If no template, fallback to JSON.
|
78
|
+
"""Render the message content as a string.
|
108
79
|
|
109
|
-
|
110
|
-
str: The final formatted string.
|
80
|
+
Delegates to the content's rendered property.
|
111
81
|
"""
|
112
|
-
|
113
|
-
if isinstance(self.template, str):
|
114
|
-
return self.template.format(**self.content)
|
115
|
-
if isinstance(self.template, Template):
|
116
|
-
return self.template.render(**self.content)
|
117
|
-
except Exception:
|
118
|
-
return ln.json_dumps(
|
119
|
-
self.content,
|
120
|
-
pretty=True,
|
121
|
-
sort_keys=True,
|
122
|
-
append_newline=True,
|
123
|
-
deterministic_sets=True,
|
124
|
-
decimal_as_float=True,
|
125
|
-
)
|
82
|
+
return self.content.rendered
|
126
83
|
|
127
|
-
@
|
128
|
-
def
|
129
|
-
|
84
|
+
@field_validator("role", mode="before")
|
85
|
+
def _validate_role(cls, v):
|
86
|
+
if isinstance(v, str):
|
87
|
+
return MessageRole(v)
|
88
|
+
if isinstance(v, MessageRole):
|
89
|
+
return v
|
90
|
+
return MessageRole.UNSET
|
130
91
|
|
131
|
-
|
132
|
-
|
133
|
-
"""
|
134
|
-
Deserialize a dictionary into a RoledMessage or subclass.
|
92
|
+
def update(self, sender=None, recipient=None, **kw):
|
93
|
+
"""Update message fields.
|
135
94
|
|
136
95
|
Args:
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
RoledMessage: A newly constructed instance.
|
96
|
+
sender: New sender role or ID.
|
97
|
+
recipient: New recipient role or ID.
|
98
|
+
**kw: Content updates to apply via from_dict() reconstruction.
|
141
99
|
"""
|
142
|
-
|
143
|
-
self
|
144
|
-
|
145
|
-
)
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
100
|
+
if sender:
|
101
|
+
self.sender = validate_sender_recipient(sender)
|
102
|
+
if recipient:
|
103
|
+
self.recipient = validate_sender_recipient(recipient)
|
104
|
+
if kw:
|
105
|
+
_dict = self.content.to_dict()
|
106
|
+
_dict.update(kw)
|
107
|
+
self.content = type(self.content).from_dict(_dict)
|
150
108
|
|
151
|
-
def
|
152
|
-
"""
|
153
|
-
Check if this message is flagged as a clone.
|
109
|
+
def clone(self) -> "RoledMessage":
|
110
|
+
"""Create a clone with a new ID but reference to original.
|
154
111
|
|
155
112
|
Returns:
|
156
|
-
|
113
|
+
A new message instance with a new ID and deep-copied content,
|
114
|
+
with a reference to the original message in metadata.
|
157
115
|
"""
|
158
|
-
|
116
|
+
# Create a new instance from dict, excluding frozen fields (id, created_at)
|
117
|
+
# This allows new id and created_at to be generated
|
118
|
+
data = self.to_dict()
|
119
|
+
original_id = data.pop("id")
|
120
|
+
data.pop("created_at") # Let new created_at be generated
|
159
121
|
|
160
|
-
|
161
|
-
|
162
|
-
Create a shallow copy of this message, possibly resetting the role.
|
122
|
+
# Create new instance
|
123
|
+
cloned = type(self).from_dict(data)
|
163
124
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
Returns:
|
168
|
-
RoledMessage: The new cloned message.
|
169
|
-
"""
|
170
|
-
instance = self.__class__(
|
171
|
-
content=self.content,
|
172
|
-
role=self.role if keep_role else MessageRole.UNSET,
|
173
|
-
metadata={"clone_from": self},
|
174
|
-
)
|
175
|
-
instance._flag = MessageFlag.MESSAGE_CLONE
|
176
|
-
return instance
|
177
|
-
|
178
|
-
def to_log(self) -> Log:
|
179
|
-
"""
|
180
|
-
Convert this message into a `Log`, preserving all current fields.
|
125
|
+
# Store reference to original in metadata
|
126
|
+
cloned.metadata["clone_from"] = str(original_id)
|
181
127
|
|
182
|
-
|
183
|
-
Log: An immutable log entry derived from this message.
|
184
|
-
"""
|
185
|
-
return Log.create(self)
|
186
|
-
|
187
|
-
@field_serializer("role")
|
188
|
-
def _serialize_role(self, value: MessageRole):
|
189
|
-
if isinstance(value, MessageRole):
|
190
|
-
return value.value
|
191
|
-
return str(value)
|
192
|
-
|
193
|
-
@field_serializer("metadata")
|
194
|
-
def _serialize_metadata(self, value: dict):
|
195
|
-
if "clone_from" in value:
|
196
|
-
origin_obj: RoledMessage = value.pop("clone_from")
|
197
|
-
origin_info = origin_obj.to_dict()
|
198
|
-
value["clone_from_info"] = {
|
199
|
-
"clone_from_info": {
|
200
|
-
"original_id": origin_info["id"],
|
201
|
-
"original_created_at": origin_info["created_at"],
|
202
|
-
"original_sender": origin_info["sender"],
|
203
|
-
"original_recipient": origin_info["recipient"],
|
204
|
-
"original_lion_class": origin_info["metadata"][
|
205
|
-
"lion_class"
|
206
|
-
],
|
207
|
-
"original_role": origin_info["role"],
|
208
|
-
}
|
209
|
-
}
|
210
|
-
return value
|
211
|
-
|
212
|
-
@field_serializer("template")
|
213
|
-
def _serialize_template(self, value: Template | str):
|
214
|
-
# We do not store or transmit the raw Template object.
|
215
|
-
if isinstance(value, Template):
|
216
|
-
return None
|
217
|
-
return value
|
128
|
+
return cloned
|
218
129
|
|
219
|
-
|
130
|
+
@property
|
131
|
+
def image_content(self) -> list[dict[str, Any]] | None:
|
220
132
|
"""
|
221
|
-
|
222
|
-
|
223
|
-
Args:
|
224
|
-
sender (SenderRecipient):
|
225
|
-
New sender or role.
|
226
|
-
recipient (SenderRecipient):
|
227
|
-
New recipient or role.
|
228
|
-
template (Template | str):
|
229
|
-
New jinja Template or format string.
|
230
|
-
**kwargs:
|
231
|
-
Additional content to merge into self.content.
|
133
|
+
Extract structured image data from the message content if it is
|
134
|
+
represented as a chat message array.
|
232
135
|
"""
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
if kwargs:
|
238
|
-
self.content.update(kwargs)
|
239
|
-
if template:
|
240
|
-
if not isinstance(template, Template | str):
|
241
|
-
raise ValueError("Template must be a Jinja2 Template or str")
|
242
|
-
self.template = template
|
243
|
-
|
244
|
-
def __str__(self) -> str:
|
245
|
-
content_preview = (
|
246
|
-
f"{str(self.content)[:75]}..."
|
247
|
-
if len(str(self.content)) > 75
|
248
|
-
else str(self.content)
|
249
|
-
)
|
250
|
-
return f"Message(role={self.role}, sender={self.sender}, content='{content_preview}')"
|
136
|
+
msg_ = self.chat_msg
|
137
|
+
if isinstance(msg_, dict) and isinstance(msg_["content"], list):
|
138
|
+
return [i for i in msg_["content"] if i["type"] == "image_url"]
|
139
|
+
return None
|
251
140
|
|
252
141
|
|
253
142
|
# File: lionagi/protocols/messages/message.py
|