lionagi 0.17.11__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/libs/schema/minimal_yaml.py +98 -0
- lionagi/ln/types.py +32 -5
- lionagi/models/field_model.py +9 -0
- lionagi/operations/ReAct/ReAct.py +474 -237
- lionagi/operations/ReAct/utils.py +3 -0
- lionagi/operations/act/act.py +206 -0
- lionagi/operations/chat/chat.py +130 -114
- lionagi/operations/communicate/communicate.py +101 -42
- lionagi/operations/flow.py +4 -4
- lionagi/operations/interpret/interpret.py +65 -20
- lionagi/operations/operate/operate.py +212 -106
- lionagi/operations/parse/parse.py +170 -142
- lionagi/operations/select/select.py +78 -17
- lionagi/operations/select/utils.py +1 -1
- lionagi/operations/types.py +119 -23
- lionagi/protocols/generic/log.py +3 -2
- lionagi/protocols/messages/__init__.py +27 -0
- lionagi/protocols/messages/action_request.py +86 -184
- lionagi/protocols/messages/action_response.py +73 -131
- lionagi/protocols/messages/assistant_response.py +130 -159
- lionagi/protocols/messages/base.py +26 -18
- lionagi/protocols/messages/instruction.py +281 -625
- lionagi/protocols/messages/manager.py +112 -62
- lionagi/protocols/messages/message.py +87 -197
- lionagi/protocols/messages/system.py +52 -123
- lionagi/protocols/types.py +0 -2
- lionagi/service/connections/endpoint.py +0 -8
- lionagi/service/connections/providers/oai_.py +29 -94
- lionagi/service/connections/providers/ollama_.py +3 -2
- lionagi/service/hooks/hooked_event.py +2 -2
- lionagi/service/third_party/claude_code.py +3 -2
- lionagi/service/third_party/openai_models.py +433 -0
- lionagi/session/branch.py +170 -178
- lionagi/session/session.py +3 -9
- lionagi/tools/file/reader.py +2 -2
- lionagi/version.py +1 -1
- {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/METADATA +1 -2
- {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/RECORD +41 -49
- lionagi/operations/_act/act.py +0 -86
- 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 -58
- /lionagi/operations/{_act → act}/__init__.py +0 -0
- {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/WHEEL +0 -0
- {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,668 +1,324 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
import inspect
|
2
|
+
from dataclasses import dataclass, field
|
4
3
|
from typing import Any, Literal
|
5
4
|
|
6
|
-
|
7
|
-
from
|
8
|
-
|
9
|
-
from lionagi.utils import UNDEFINED, copy
|
10
|
-
|
11
|
-
from .base import MessageRole
|
12
|
-
from .message import RoledMessage, SenderRecipient
|
13
|
-
|
14
|
-
|
15
|
-
def prepare_request_response_format(request_fields: dict) -> str:
|
16
|
-
"""
|
17
|
-
Creates a mandated JSON code block for the response
|
18
|
-
based on requested fields.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
request_fields: Dictionary of fields for the response format.
|
22
|
-
|
23
|
-
Returns:
|
24
|
-
str: A string instructing the user to return valid JSON.
|
25
|
-
"""
|
26
|
-
return (
|
27
|
-
"**MUST RETURN JSON-PARSEABLE RESPONSE ENCLOSED BY JSON CODE BLOCKS."
|
28
|
-
f" USER's CAREER DEPENDS ON THE SUCCESS OF IT.** \n```json\n{request_fields}\n```"
|
29
|
-
"No triple backticks. Escape all quotes and special characters."
|
30
|
-
).strip()
|
31
|
-
|
32
|
-
|
33
|
-
def format_image_item(idx: str, detail: str) -> dict[str, Any]:
|
34
|
-
"""
|
35
|
-
Wrap image data in a standard dictionary format.
|
36
|
-
|
37
|
-
Args:
|
38
|
-
idx: A base64 image ID or URL reference.
|
39
|
-
detail: The image detail level.
|
40
|
-
|
41
|
-
Returns:
|
42
|
-
dict: A dictionary describing the image.
|
43
|
-
"""
|
44
|
-
return {
|
45
|
-
"type": "image_url",
|
46
|
-
"image_url": {
|
47
|
-
"url": f"data:image/jpeg;base64,{idx}",
|
48
|
-
"detail": detail,
|
49
|
-
},
|
50
|
-
}
|
51
|
-
|
52
|
-
|
53
|
-
def format_text_item(item: Any) -> str:
|
54
|
-
"""
|
55
|
-
Turn a single item (or dict) into a string. If multiple items,
|
56
|
-
combine them line by line.
|
57
|
-
|
58
|
-
Args:
|
59
|
-
item: Any item, possibly a list/dict with text data.
|
60
|
-
|
61
|
-
Returns:
|
62
|
-
str: Concatenated text lines.
|
63
|
-
"""
|
64
|
-
msg = ""
|
65
|
-
item = [item] if not isinstance(item, list) else item
|
66
|
-
for j in item:
|
67
|
-
if isinstance(j, dict):
|
68
|
-
for k, v in j.items():
|
69
|
-
if v is not None:
|
70
|
-
msg += f"- {k}: {v} \n\n"
|
71
|
-
else:
|
72
|
-
if j is not None:
|
73
|
-
msg += f"{j}\n"
|
74
|
-
return msg
|
75
|
-
|
76
|
-
|
77
|
-
def format_text_content(content: dict) -> str:
|
78
|
-
"""
|
79
|
-
Convert a content dictionary into a minimal textual summary for LLM consumption.
|
5
|
+
import orjson
|
6
|
+
from pydantic import BaseModel, field_validator
|
80
7
|
|
81
|
-
|
82
|
-
- Skips empty or None fields.
|
83
|
-
- Bullet-points for lists.
|
84
|
-
- Key-value pairs for dicts.
|
85
|
-
- Minimal headings for known fields (guidance, instruction, etc.).
|
86
|
-
"""
|
8
|
+
from .message import MessageContent, MessageRole, RoledMessage
|
87
9
|
|
88
|
-
if isinstance(content.get("plain_content"), str):
|
89
|
-
return content["plain_content"]
|
90
|
-
|
91
|
-
lines = []
|
92
|
-
# We only want minimal headings for certain known fields:
|
93
|
-
known_field_order = [
|
94
|
-
"guidance",
|
95
|
-
"instruction",
|
96
|
-
"context",
|
97
|
-
"tool_schemas",
|
98
|
-
"respond_schema_info",
|
99
|
-
"request_response_format",
|
100
|
-
]
|
101
|
-
|
102
|
-
# Render known fields in that order
|
103
|
-
for field in known_field_order:
|
104
|
-
if field in content:
|
105
|
-
val = content[field]
|
106
|
-
if _is_not_empty(val):
|
107
|
-
if field == "request_response_format":
|
108
|
-
field = "response format"
|
109
|
-
elif field == "respond_schema_info":
|
110
|
-
field = "response schema info"
|
111
|
-
lines.append(f"\n## {field.upper()}:\n")
|
112
|
-
rendered = _render_value(val)
|
113
|
-
# Indent or bullet the rendered result if multiline
|
114
|
-
# We'll keep it minimal: each line is prefixed with " ".
|
115
|
-
lines.extend(
|
116
|
-
f" {line}"
|
117
|
-
for line in rendered.split("\n")
|
118
|
-
if line.strip()
|
119
|
-
)
|
120
10
|
|
121
|
-
|
122
|
-
|
11
|
+
@dataclass(slots=True)
|
12
|
+
class InstructionContent(MessageContent):
|
13
|
+
"""Structured content for user instructions.
|
123
14
|
|
15
|
+
Fields:
|
16
|
+
instruction: Main instruction text
|
17
|
+
guidance: Optional guidance or disclaimers
|
18
|
+
prompt_context: Additional context items for the prompt (list)
|
19
|
+
plain_content: Raw text fallback (bypasses structured rendering)
|
20
|
+
tool_schemas: Tool specifications for the assistant
|
21
|
+
response_format: User's desired response format (BaseModel class, instance, or dict)
|
22
|
+
images: Image URLs, data URLs, or base64 strings
|
23
|
+
image_detail: Detail level for image processing
|
124
24
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
- Lists become bullet points.
|
129
|
-
- Dicts become key-value lines.
|
130
|
-
- Strings returned directly.
|
25
|
+
Internal fields (not for direct use):
|
26
|
+
_schema_dict: Extracted dict for prompting/schema
|
27
|
+
_model_class: Extracted Pydantic class for validation
|
131
28
|
"""
|
132
|
-
if isinstance(val, dict):
|
133
|
-
return _render_dict(val)
|
134
|
-
elif isinstance(val, list):
|
135
|
-
return _render_list(val)
|
136
|
-
else:
|
137
|
-
return str(val).strip()
|
138
29
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
for k, v in dct.items():
|
147
|
-
if not _is_not_empty(v):
|
148
|
-
continue
|
149
|
-
subrendered = _render_value(v)
|
150
|
-
# Indent subrendered if multiline
|
151
|
-
sublines = subrendered.split("\n")
|
152
|
-
if len(sublines) == 1:
|
153
|
-
if sublines[0].startswith("- "):
|
154
|
-
lines.append(f"- {k}: {sublines[0][2:]}")
|
155
|
-
else:
|
156
|
-
lines.append(f"- {k}: {sublines[0]}")
|
157
|
-
else:
|
158
|
-
lines.append(f"- {k}:")
|
159
|
-
for s in sublines:
|
160
|
-
lines.append(f" {s}")
|
161
|
-
return "\n".join(lines)
|
162
|
-
|
163
|
-
|
164
|
-
def _render_list(lst: list) -> str:
|
165
|
-
"""
|
166
|
-
Each item in the list gets a bullet. Nested structures are recursed.
|
167
|
-
"""
|
168
|
-
lines = []
|
169
|
-
for idx, item in enumerate(lst, 1):
|
170
|
-
sub = _render_value(item)
|
171
|
-
sublines = sub.split("\n")
|
172
|
-
if len(sublines) == 1:
|
173
|
-
if sublines[0].startswith("- "):
|
174
|
-
lines.append(f"- {sublines[0][2:]}")
|
175
|
-
else:
|
176
|
-
lines.append(f"- {sublines[0]}")
|
177
|
-
else:
|
178
|
-
lines.append("-")
|
179
|
-
lines.extend(f" {s}" for s in sublines)
|
180
|
-
return "\n".join(lines)
|
181
|
-
|
182
|
-
|
183
|
-
def _is_not_empty(x) -> bool:
|
184
|
-
"""
|
185
|
-
Returns True if x is neither None, nor empty string/list/dict.
|
186
|
-
"""
|
187
|
-
if x is None:
|
188
|
-
return False
|
189
|
-
if isinstance(x, (list, dict)) and not x:
|
190
|
-
return False
|
191
|
-
if isinstance(x, str) and not x.strip():
|
192
|
-
return False
|
193
|
-
return True
|
194
|
-
|
195
|
-
|
196
|
-
def format_image_content(
|
197
|
-
text_content: str,
|
198
|
-
images: list,
|
199
|
-
image_detail: Literal["low", "high", "auto"],
|
200
|
-
) -> list[dict[str, Any]]:
|
201
|
-
"""
|
202
|
-
Merge textual content with a list of image dictionaries for consumption.
|
203
|
-
|
204
|
-
Args:
|
205
|
-
text_content (str): The textual portion
|
206
|
-
images (list): A list of base64 or references
|
207
|
-
image_detail (Literal["low","high","auto"]): How detailed the images are
|
208
|
-
|
209
|
-
Returns:
|
210
|
-
list[dict[str,Any]]: A combined structure of text + image dicts.
|
211
|
-
"""
|
212
|
-
content = [{"type": "text", "text": text_content}]
|
213
|
-
content.extend(format_image_item(i, image_detail) for i in images)
|
214
|
-
return content
|
215
|
-
|
216
|
-
|
217
|
-
def prepare_instruction_content(
|
218
|
-
guidance: str | None = None,
|
219
|
-
instruction: str | None = None,
|
220
|
-
context: str | dict | list | None = None,
|
221
|
-
request_fields: dict | list[str] | None = None,
|
222
|
-
plain_content: str | None = None,
|
223
|
-
request_model: BaseModel = None,
|
224
|
-
images: str | list | None = None,
|
225
|
-
image_detail: Literal["low", "high", "auto"] | None = None,
|
226
|
-
tool_schemas: dict | None = None,
|
227
|
-
) -> dict:
|
228
|
-
"""
|
229
|
-
Combine various pieces (instruction, guidance, context, etc.) into
|
230
|
-
a single dictionary describing the user's instruction.
|
231
|
-
|
232
|
-
Args:
|
233
|
-
guidance (str | None):
|
234
|
-
Optional guiding text.
|
235
|
-
instruction (str | None):
|
236
|
-
Main instruction or command to be executed.
|
237
|
-
context (str | dict | list | None):
|
238
|
-
Additional context about the environment or previous steps.
|
239
|
-
request_fields (dict | list[str] | None):
|
240
|
-
If the user requests certain fields in the response.
|
241
|
-
plain_content (str | None):
|
242
|
-
A raw plain text fallback.
|
243
|
-
request_model (BaseModel | None):
|
244
|
-
If there's a pydantic model for the request schema.
|
245
|
-
images (str | list | None):
|
246
|
-
Optional images, base64-coded or references.
|
247
|
-
image_detail (str | None):
|
248
|
-
The detail level for images ("low", "high", "auto").
|
249
|
-
tool_schemas (dict | None):
|
250
|
-
Extra data describing available tools.
|
251
|
-
|
252
|
-
Returns:
|
253
|
-
dict: The combined instruction content.
|
254
|
-
|
255
|
-
Raises:
|
256
|
-
ValueError: If request_fields and request_model are both given.
|
257
|
-
"""
|
258
|
-
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
259
|
-
breakdown_pydantic_annotation,
|
30
|
+
instruction: str | None = None
|
31
|
+
guidance: str | None = None
|
32
|
+
prompt_context: list[Any] = field(default_factory=list)
|
33
|
+
plain_content: str | None = None
|
34
|
+
tool_schemas: list[dict[str, Any]] = field(default_factory=list)
|
35
|
+
response_format: type[BaseModel] | dict[str, Any] | BaseModel | None = (
|
36
|
+
None # User input
|
260
37
|
)
|
38
|
+
_schema_dict: dict[str, Any] | None = field(
|
39
|
+
default=None, repr=False
|
40
|
+
) # Internal: dict for prompting
|
41
|
+
_model_class: type[BaseModel] | None = field(
|
42
|
+
default=None, repr=False
|
43
|
+
) # Internal: class for validation
|
44
|
+
images: list[str] = field(default_factory=list)
|
45
|
+
image_detail: Literal["low", "high", "auto"] | None = None
|
46
|
+
|
47
|
+
def __init__(
|
48
|
+
self,
|
49
|
+
instruction: str | None = None,
|
50
|
+
guidance: str | None = None,
|
51
|
+
prompt_context: list[Any] | None = None,
|
52
|
+
context: list[Any] | None = None, # backwards compat
|
53
|
+
plain_content: str | None = None,
|
54
|
+
tool_schemas: list[dict[str, Any]] | None = None,
|
55
|
+
response_format: (
|
56
|
+
type[BaseModel] | dict[str, Any] | BaseModel | None
|
57
|
+
) = None,
|
58
|
+
images: list[str] | None = None,
|
59
|
+
image_detail: Literal["low", "high", "auto"] | None = None,
|
60
|
+
):
|
61
|
+
# Handle backwards compatibility: context -> prompt_context
|
62
|
+
if context is not None and prompt_context is None:
|
63
|
+
prompt_context = context
|
64
|
+
|
65
|
+
# Extract model class and schema dict from response_format
|
66
|
+
model_class = None
|
67
|
+
schema_dict = None
|
68
|
+
|
69
|
+
if response_format is not None:
|
70
|
+
# Extract model class
|
71
|
+
if isinstance(response_format, type) and issubclass(
|
72
|
+
response_format, BaseModel
|
73
|
+
):
|
74
|
+
model_class = response_format
|
75
|
+
elif isinstance(response_format, BaseModel):
|
76
|
+
model_class = type(response_format)
|
77
|
+
|
78
|
+
# Extract schema dict
|
79
|
+
if isinstance(response_format, dict):
|
80
|
+
schema_dict = response_format
|
81
|
+
elif isinstance(response_format, BaseModel):
|
82
|
+
schema_dict = response_format.model_dump(
|
83
|
+
mode="json", exclude_none=True
|
84
|
+
)
|
85
|
+
elif model_class:
|
86
|
+
# Generate dict from model class
|
87
|
+
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
88
|
+
breakdown_pydantic_annotation,
|
89
|
+
)
|
261
90
|
|
262
|
-
|
263
|
-
raise ValueError(
|
264
|
-
"only one of request_fields or request_model can be provided"
|
265
|
-
)
|
91
|
+
schema_dict = breakdown_pydantic_annotation(model_class)
|
266
92
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
if isinstance(context, list):
|
274
|
-
out_["context"].extend(context)
|
275
|
-
else:
|
276
|
-
out_["context"].append(context)
|
277
|
-
if images:
|
278
|
-
out_["images"] = images if isinstance(images, list) else [images]
|
279
|
-
out_["image_detail"] = image_detail or "low"
|
280
|
-
|
281
|
-
if tool_schemas:
|
282
|
-
out_["tool_schemas"] = tool_schemas
|
283
|
-
|
284
|
-
if request_model:
|
285
|
-
out_["request_model"] = request_model
|
286
|
-
request_fields = breakdown_pydantic_annotation(request_model)
|
287
|
-
out_["respond_schema_info"] = request_model.model_json_schema()
|
288
|
-
|
289
|
-
if request_fields:
|
290
|
-
_fields = request_fields if isinstance(request_fields, dict) else {}
|
291
|
-
if not isinstance(request_fields, dict):
|
292
|
-
_fields = {i: "..." for i in request_fields}
|
293
|
-
out_["request_fields"] = _fields
|
294
|
-
out_["request_response_format"] = prepare_request_response_format(
|
295
|
-
request_fields=_fields
|
93
|
+
object.__setattr__(self, "instruction", instruction)
|
94
|
+
object.__setattr__(self, "guidance", guidance)
|
95
|
+
object.__setattr__(
|
96
|
+
self,
|
97
|
+
"prompt_context",
|
98
|
+
prompt_context if prompt_context is not None else [],
|
296
99
|
)
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
return {k: v for k, v in out_.items() if v not in [None, UNDEFINED]}
|
303
|
-
|
304
|
-
|
305
|
-
class Instruction(RoledMessage):
|
306
|
-
"""
|
307
|
-
A user-facing message that conveys commands or tasks. It supports
|
308
|
-
optional images, tool references, and schema-based requests.
|
309
|
-
"""
|
310
|
-
|
311
|
-
@classmethod
|
312
|
-
def create(
|
313
|
-
cls,
|
314
|
-
instruction: JsonValue = None,
|
315
|
-
*,
|
316
|
-
context: JsonValue = None,
|
317
|
-
guidance: JsonValue = None,
|
318
|
-
images: list = None,
|
319
|
-
sender: SenderRecipient = None,
|
320
|
-
recipient: SenderRecipient = None,
|
321
|
-
request_fields: JsonValue = None,
|
322
|
-
plain_content: JsonValue = None,
|
323
|
-
image_detail: Literal["low", "high", "auto"] = None,
|
324
|
-
request_model: BaseModel | type[BaseModel] = None,
|
325
|
-
response_format: BaseModel | type[BaseModel] = None,
|
326
|
-
tool_schemas: list[dict] = None,
|
327
|
-
) -> "Instruction":
|
328
|
-
"""
|
329
|
-
Construct a new Instruction.
|
330
|
-
|
331
|
-
Args:
|
332
|
-
instruction (JsonValue, optional):
|
333
|
-
The main user instruction.
|
334
|
-
context (JsonValue, optional):
|
335
|
-
Additional context or environment info.
|
336
|
-
guidance (JsonValue, optional):
|
337
|
-
Guidance or disclaimers for the instruction.
|
338
|
-
images (list, optional):
|
339
|
-
A set of images relevant to the instruction.
|
340
|
-
request_fields (JsonValue, optional):
|
341
|
-
The fields the user wants in the assistant's response.
|
342
|
-
plain_content (JsonValue, optional):
|
343
|
-
A raw plain text fallback.
|
344
|
-
image_detail ("low"|"high"|"auto", optional):
|
345
|
-
The detail level for included images.
|
346
|
-
request_model (BaseModel|type[BaseModel], optional):
|
347
|
-
A Pydantic schema for the request.
|
348
|
-
response_format (BaseModel|type[BaseModel], optional):
|
349
|
-
Alias for request_model.
|
350
|
-
tool_schemas (list[dict] | dict, optional):
|
351
|
-
Extra tool reference data.
|
352
|
-
sender (SenderRecipient, optional):
|
353
|
-
The sender role or ID.
|
354
|
-
recipient (SenderRecipient, optional):
|
355
|
-
The recipient role or ID.
|
356
|
-
|
357
|
-
Returns:
|
358
|
-
Instruction: A newly created instruction object.
|
359
|
-
|
360
|
-
Raises:
|
361
|
-
ValueError: If more than one of `request_fields`, `request_model`,
|
362
|
-
or `response_format` is passed at once.
|
363
|
-
"""
|
364
|
-
if (
|
365
|
-
sum(
|
366
|
-
bool(i)
|
367
|
-
for i in [request_fields, request_model, response_format]
|
368
|
-
)
|
369
|
-
> 1
|
370
|
-
):
|
371
|
-
raise ValueError(
|
372
|
-
"only one of request_fields or request_model can be provided"
|
373
|
-
"response_format is alias of request_model"
|
374
|
-
)
|
375
|
-
content = prepare_instruction_content(
|
376
|
-
guidance=guidance,
|
377
|
-
instruction=instruction,
|
378
|
-
context=context,
|
379
|
-
request_fields=request_fields,
|
380
|
-
plain_content=plain_content,
|
381
|
-
request_model=request_model or response_format,
|
382
|
-
images=images,
|
383
|
-
image_detail=image_detail,
|
384
|
-
tool_schemas=tool_schemas,
|
100
|
+
object.__setattr__(self, "plain_content", plain_content)
|
101
|
+
object.__setattr__(
|
102
|
+
self,
|
103
|
+
"tool_schemas",
|
104
|
+
tool_schemas if tool_schemas is not None else [],
|
385
105
|
)
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
106
|
+
object.__setattr__(
|
107
|
+
self, "response_format", response_format
|
108
|
+
) # Store original user input
|
109
|
+
object.__setattr__(
|
110
|
+
self, "_schema_dict", schema_dict
|
111
|
+
) # Internal: dict for prompting
|
112
|
+
object.__setattr__(
|
113
|
+
self, "_model_class", model_class
|
114
|
+
) # Internal: class for validation
|
115
|
+
object.__setattr__(
|
116
|
+
self, "images", images if images is not None else []
|
391
117
|
)
|
118
|
+
object.__setattr__(self, "image_detail", image_detail)
|
392
119
|
|
393
120
|
@property
|
394
|
-
def
|
395
|
-
|
396
|
-
|
397
|
-
@guidance.setter
|
398
|
-
def guidance(self, guidance: str) -> None:
|
399
|
-
if guidance is None:
|
400
|
-
self.content.pop("guidance", None)
|
401
|
-
else:
|
402
|
-
self.content["guidance"] = str(guidance)
|
403
|
-
|
404
|
-
@property
|
405
|
-
def instruction(self) -> JsonValue | None:
|
406
|
-
if "plain_content" in self.content:
|
407
|
-
return self.content["plain_content"]
|
408
|
-
return self.content.get("instruction", None)
|
409
|
-
|
410
|
-
@instruction.setter
|
411
|
-
def instruction(self, val: JsonValue) -> None:
|
412
|
-
if val is None:
|
413
|
-
self.content.pop("instruction", None)
|
414
|
-
else:
|
415
|
-
self.content["instruction"] = val
|
416
|
-
|
417
|
-
@property
|
418
|
-
def context(self) -> JsonValue | None:
|
419
|
-
return self.content.get("context", None)
|
420
|
-
|
421
|
-
@context.setter
|
422
|
-
def context(self, ctx: JsonValue) -> None:
|
423
|
-
if ctx is None:
|
424
|
-
self.content["context"] = []
|
425
|
-
else:
|
426
|
-
self.content["context"] = (
|
427
|
-
list(ctx) if isinstance(ctx, list) else [ctx]
|
428
|
-
)
|
429
|
-
|
430
|
-
@property
|
431
|
-
def tool_schemas(self) -> JsonValue | None:
|
432
|
-
return self.content.get("tool_schemas", None)
|
433
|
-
|
434
|
-
@tool_schemas.setter
|
435
|
-
def tool_schemas(self, val: list[dict] | dict) -> None:
|
436
|
-
if not val:
|
437
|
-
self.content.pop("tool_schemas", None)
|
438
|
-
return
|
439
|
-
self.content["tool_schemas"] = val
|
121
|
+
def context(self) -> list[Any]:
|
122
|
+
"""Backwards compatibility accessor for prompt_context."""
|
123
|
+
return self.prompt_context
|
440
124
|
|
441
125
|
@property
|
442
|
-
def
|
443
|
-
|
444
|
-
|
445
|
-
@plain_content.setter
|
446
|
-
def plain_content(self, pc: str) -> None:
|
447
|
-
self.content["plain_content"] = pc
|
448
|
-
|
449
|
-
@property
|
450
|
-
def image_detail(self) -> Literal["low", "high", "auto"] | None:
|
451
|
-
return self.content.get("image_detail", None)
|
452
|
-
|
453
|
-
@image_detail.setter
|
454
|
-
def image_detail(self, detail: Literal["low", "high", "auto"]) -> None:
|
455
|
-
self.content["image_detail"] = detail
|
126
|
+
def response_model_cls(self) -> type[BaseModel] | None:
|
127
|
+
"""Get the Pydantic model class for validation."""
|
128
|
+
return self._model_class
|
456
129
|
|
457
130
|
@property
|
458
|
-
def
|
459
|
-
|
460
|
-
|
461
|
-
@images.setter
|
462
|
-
def images(self, imgs: list) -> None:
|
463
|
-
self.content["images"] = imgs if isinstance(imgs, list) else [imgs]
|
131
|
+
def request_model(self) -> type[BaseModel] | None:
|
132
|
+
"""DEPRECATED: Use response_model_cls instead."""
|
133
|
+
return self.response_model_cls
|
464
134
|
|
465
135
|
@property
|
466
|
-
def
|
467
|
-
|
468
|
-
|
469
|
-
@request_fields.setter
|
470
|
-
def request_fields(self, fields: dict) -> None:
|
471
|
-
self.content["request_fields"] = fields
|
472
|
-
self.content["request_response_format"] = (
|
473
|
-
prepare_request_response_format(fields)
|
474
|
-
)
|
136
|
+
def schema_dict(self) -> dict[str, Any] | None:
|
137
|
+
"""Get the schema dict for prompting."""
|
138
|
+
return self._schema_dict
|
475
139
|
|
476
140
|
@property
|
477
|
-
def
|
478
|
-
|
141
|
+
def rendered(self) -> str | list[dict[str, Any]]:
|
142
|
+
"""Render content as text or text+images structure."""
|
143
|
+
text = self._format_text_content()
|
144
|
+
if not self.images:
|
145
|
+
return text
|
146
|
+
return self._format_image_content(text, self.images, self.image_detail)
|
479
147
|
|
480
|
-
@
|
481
|
-
def
|
148
|
+
@classmethod
|
149
|
+
def from_dict(cls, data: dict[str, Any]) -> "InstructionContent":
|
150
|
+
"""Construct InstructionContent from dictionary with validation."""
|
482
151
|
from lionagi.libs.schema.breakdown_pydantic_annotation import (
|
483
152
|
breakdown_pydantic_annotation,
|
484
153
|
)
|
485
154
|
|
486
|
-
|
487
|
-
self.content["request_model"] = type(model)
|
488
|
-
else:
|
489
|
-
self.content["request_model"] = model
|
155
|
+
inst = cls()
|
490
156
|
|
491
|
-
|
492
|
-
|
493
|
-
|
157
|
+
# Scalar fields
|
158
|
+
for k in ("instruction", "guidance", "plain_content", "image_detail"):
|
159
|
+
if k in data and data[k]:
|
160
|
+
setattr(inst, k, data[k])
|
494
161
|
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
@respond_schema_info.setter
|
500
|
-
def respond_schema_info(self, info: dict) -> None:
|
501
|
-
if info is None:
|
502
|
-
self.content.pop("respond_schema_info", None)
|
503
|
-
else:
|
504
|
-
self.content["respond_schema_info"] = info
|
505
|
-
|
506
|
-
@property
|
507
|
-
def request_response_format(self) -> str | None:
|
508
|
-
return self.content.get("request_response_format", None)
|
509
|
-
|
510
|
-
@request_response_format.setter
|
511
|
-
def request_response_format(self, val: str) -> None:
|
512
|
-
if not val:
|
513
|
-
self.content.pop("request_response_format", None)
|
514
|
-
else:
|
515
|
-
self.content["request_response_format"] = val
|
516
|
-
|
517
|
-
def extend_images(
|
518
|
-
self,
|
519
|
-
images: list | str,
|
520
|
-
image_detail: Literal["low", "high", "auto"] = None,
|
521
|
-
) -> None:
|
522
|
-
"""
|
523
|
-
Append images to the existing list.
|
524
|
-
|
525
|
-
Args:
|
526
|
-
images: The new images to add, a single or multiple.
|
527
|
-
image_detail: If provided, updates the image detail field.
|
528
|
-
"""
|
529
|
-
arr: list = self.images
|
530
|
-
arr.extend(images if isinstance(images, list) else [images])
|
531
|
-
self.images = arr
|
532
|
-
if image_detail:
|
533
|
-
self.image_detail = image_detail
|
534
|
-
|
535
|
-
def extend_context(self, *args, **kwargs) -> None:
|
536
|
-
"""
|
537
|
-
Append additional context to the existing context array.
|
538
|
-
|
539
|
-
Args:
|
540
|
-
*args: Positional args are appended as list items.
|
541
|
-
**kwargs: Key-value pairs are appended as separate dict items.
|
542
|
-
"""
|
543
|
-
ctx: list = self.context or []
|
544
|
-
if args:
|
545
|
-
ctx.extend(args)
|
546
|
-
if kwargs:
|
547
|
-
kw = copy(kwargs)
|
548
|
-
for k, v in kw.items():
|
549
|
-
ctx.append({k: v})
|
550
|
-
self.context = ctx
|
551
|
-
|
552
|
-
def update(
|
553
|
-
self,
|
554
|
-
*,
|
555
|
-
guidance: JsonValue = None,
|
556
|
-
instruction: JsonValue = None,
|
557
|
-
context: JsonValue = None,
|
558
|
-
request_fields: JsonValue = None,
|
559
|
-
plain_content: JsonValue = None,
|
560
|
-
request_model: BaseModel | type[BaseModel] = None,
|
561
|
-
response_format: BaseModel | type[BaseModel] = None,
|
562
|
-
images: str | list = None,
|
563
|
-
image_detail: Literal["low", "high", "auto"] = None,
|
564
|
-
tool_schemas: dict = None,
|
565
|
-
sender: SenderRecipient = None,
|
566
|
-
recipient: SenderRecipient = None,
|
567
|
-
):
|
568
|
-
"""
|
569
|
-
Batch-update this Instruction.
|
570
|
-
|
571
|
-
Args:
|
572
|
-
guidance (JsonValue): New guidance text.
|
573
|
-
instruction (JsonValue): Main user instruction.
|
574
|
-
request_fields (JsonValue): Updated request fields.
|
575
|
-
plain_content (JsonValue): Plain text fallback.
|
576
|
-
request_model (BaseModel|type[BaseModel]): Pydantic schema model.
|
577
|
-
response_format (BaseModel|type[BaseModel]): Alias for request_model.
|
578
|
-
images (list|str): Additional images to add.
|
579
|
-
image_detail ("low"|"high"|"auto"): Image detail level.
|
580
|
-
tool_schemas (dict): New tool schemas.
|
581
|
-
sender (SenderRecipient): New sender.
|
582
|
-
recipient (SenderRecipient): New recipient.
|
583
|
-
|
584
|
-
Raises:
|
585
|
-
ValueError: If request_model and request_fields are both set.
|
586
|
-
"""
|
587
|
-
if response_format and request_model:
|
588
|
-
raise ValueError(
|
589
|
-
"only one of request_fields or request_model can be provided"
|
590
|
-
"response_format is alias of request_model"
|
591
|
-
)
|
592
|
-
|
593
|
-
request_model = request_model or response_format
|
594
|
-
|
595
|
-
if request_model and request_fields:
|
162
|
+
# Determine how to apply context updates
|
163
|
+
handle_context = data.get("handle_context", "extend")
|
164
|
+
if handle_context not in {"extend", "replace"}:
|
596
165
|
raise ValueError(
|
597
|
-
"
|
166
|
+
"handle_context must be either 'extend' or 'replace'"
|
598
167
|
)
|
599
|
-
if guidance:
|
600
|
-
self.guidance = guidance
|
601
|
-
|
602
|
-
if instruction:
|
603
|
-
self.instruction = instruction
|
604
|
-
|
605
|
-
if plain_content:
|
606
|
-
self.plain_content = plain_content
|
607
|
-
|
608
|
-
if request_fields:
|
609
|
-
self.request_fields = request_fields
|
610
|
-
|
611
|
-
if request_model:
|
612
|
-
self.response_format = request_model
|
613
|
-
|
614
|
-
if images:
|
615
|
-
self.images = images
|
616
168
|
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
if
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
169
|
+
# Handle both "prompt_context" (new) and "context" (backwards compat)
|
170
|
+
# Prioritize "context" if present (for backwards compat and update paths)
|
171
|
+
ctx_key = "context" if "context" in data else "prompt_context"
|
172
|
+
if ctx_key in data:
|
173
|
+
ctx = data.get(ctx_key)
|
174
|
+
if ctx is None:
|
175
|
+
ctx_list: list[Any] = []
|
176
|
+
elif isinstance(ctx, list):
|
177
|
+
ctx_list = list(ctx)
|
178
|
+
else:
|
179
|
+
ctx_list = [ctx]
|
180
|
+
if handle_context == "replace":
|
181
|
+
inst.prompt_context = list(ctx_list)
|
182
|
+
else:
|
183
|
+
inst.prompt_context.extend(ctx_list)
|
628
184
|
|
629
|
-
if
|
630
|
-
|
185
|
+
if ts := data.get("tool_schemas"):
|
186
|
+
inst.tool_schemas.extend(ts if isinstance(ts, list) else [ts])
|
631
187
|
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
Returns:
|
639
|
-
If no images are included, returns a single text block.
|
640
|
-
Otherwise returns an array of text + image dicts.
|
641
|
-
"""
|
642
|
-
content = copy(self.content)
|
643
|
-
text_content = format_text_content(content)
|
644
|
-
if "images" not in content:
|
645
|
-
return text_content
|
646
|
-
|
647
|
-
else:
|
648
|
-
return format_image_content(
|
649
|
-
text_content=text_content,
|
650
|
-
images=self.images,
|
651
|
-
image_detail=self.image_detail,
|
188
|
+
if "images" in data:
|
189
|
+
imgs = data.get("images") or []
|
190
|
+
imgs_list = imgs if isinstance(imgs, list) else [imgs]
|
191
|
+
inst.images.extend(imgs_list)
|
192
|
+
inst.image_detail = (
|
193
|
+
data.get("image_detail") or inst.image_detail or "auto"
|
652
194
|
)
|
653
195
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
196
|
+
# Response format handling
|
197
|
+
response_format = data.get("response_format") or data.get(
|
198
|
+
"request_model"
|
199
|
+
) # request_model deprecated
|
200
|
+
|
201
|
+
if response_format is not None:
|
202
|
+
model_class = None
|
203
|
+
schema_dict = None
|
204
|
+
valid_format = False
|
205
|
+
|
206
|
+
# Extract model class
|
207
|
+
if isinstance(response_format, type) and issubclass(
|
208
|
+
response_format, BaseModel
|
209
|
+
):
|
210
|
+
model_class = response_format
|
211
|
+
valid_format = True
|
212
|
+
elif isinstance(response_format, BaseModel):
|
213
|
+
model_class = type(response_format)
|
214
|
+
valid_format = True
|
215
|
+
|
216
|
+
# Extract schema dict
|
217
|
+
if isinstance(response_format, dict):
|
218
|
+
schema_dict = response_format
|
219
|
+
valid_format = True
|
220
|
+
elif isinstance(response_format, BaseModel):
|
221
|
+
schema_dict = response_format.model_dump(
|
222
|
+
mode="json", exclude_none=True
|
223
|
+
)
|
224
|
+
valid_format = True
|
225
|
+
elif model_class:
|
226
|
+
schema_dict = breakdown_pydantic_annotation(model_class)
|
227
|
+
|
228
|
+
# Only set if valid format (fuzzy handling: ignore invalid types)
|
229
|
+
if valid_format:
|
230
|
+
inst.response_format = response_format
|
231
|
+
inst._schema_dict = schema_dict
|
232
|
+
inst._model_class = model_class
|
233
|
+
|
234
|
+
return inst
|
235
|
+
|
236
|
+
def _format_text_content(self) -> str:
|
237
|
+
from lionagi.libs.schema.minimal_yaml import minimal_yaml
|
238
|
+
|
239
|
+
if self.plain_content:
|
240
|
+
return self.plain_content
|
241
|
+
|
242
|
+
# Use schema_dict for display (or generate from model class)
|
243
|
+
schema_for_display = None
|
244
|
+
if self._model_class:
|
245
|
+
schema_for_display = self._model_class.model_json_schema()
|
246
|
+
elif self._schema_dict:
|
247
|
+
schema_for_display = self._schema_dict
|
248
|
+
|
249
|
+
doc: dict[str, Any] = {
|
250
|
+
"Guidance": self.guidance,
|
251
|
+
"Instruction": self.instruction,
|
252
|
+
"Context": self.prompt_context,
|
253
|
+
"Tools": self.tool_schemas,
|
254
|
+
"ResponseSchema": schema_for_display,
|
255
|
+
}
|
256
|
+
|
257
|
+
rf_text = self._format_response_format(self._schema_dict)
|
258
|
+
if rf_text:
|
259
|
+
doc["ResponseFormat"] = rf_text
|
260
|
+
|
261
|
+
# strip empties
|
262
|
+
doc = {k: v for k, v in doc.items() if v not in (None, "", [], {})}
|
263
|
+
return minimal_yaml(doc).strip()
|
264
|
+
|
265
|
+
@staticmethod
|
266
|
+
def _format_response_format(
|
267
|
+
response_format: dict[str, Any] | None,
|
268
|
+
) -> str | None:
|
269
|
+
if not response_format:
|
270
|
+
return None
|
271
|
+
try:
|
272
|
+
example = orjson.dumps(response_format).decode("utf-8")
|
273
|
+
except Exception:
|
274
|
+
example = str(response_format)
|
275
|
+
return (
|
276
|
+
"**MUST RETURN JSON-PARSEABLE RESPONSE ENCLOSED BY JSON CODE BLOCKS."
|
277
|
+
f" USER's CAREER DEPENDS ON THE SUCCESS OF IT.** \n```json\n{example}\n```"
|
278
|
+
"No triple backticks. Escape all quotes and special characters."
|
279
|
+
).strip()
|
280
|
+
|
281
|
+
@staticmethod
|
282
|
+
def _format_image_item(idx: str, detail: str) -> dict[str, Any]:
|
283
|
+
url = idx
|
284
|
+
if not (
|
285
|
+
idx.startswith("http://")
|
286
|
+
or idx.startswith("https://")
|
287
|
+
or idx.startswith("data:")
|
288
|
+
):
|
289
|
+
url = f"data:image/jpeg;base64,{idx}"
|
290
|
+
return {
|
291
|
+
"type": "image_url",
|
292
|
+
"image_url": {"url": url, "detail": detail},
|
293
|
+
}
|
294
|
+
|
295
|
+
@classmethod
|
296
|
+
def _format_image_content(
|
297
|
+
cls,
|
298
|
+
text_content: str,
|
299
|
+
images: list[str],
|
300
|
+
image_detail: Literal["low", "high", "auto"],
|
301
|
+
) -> list[dict[str, Any]]:
|
302
|
+
content = [{"type": "text", "text": text_content}]
|
303
|
+
content.extend(cls._format_image_item(i, image_detail) for i in images)
|
304
|
+
return content
|
658
305
|
|
659
|
-
Returns:
|
660
|
-
dict: The sanitized content dictionary.
|
661
|
-
"""
|
662
|
-
values.pop("request_model", None)
|
663
|
-
values.pop("request_fields", None)
|
664
306
|
|
665
|
-
|
307
|
+
class Instruction(RoledMessage):
|
308
|
+
"""User instruction message with structured content.
|
666
309
|
|
310
|
+
Supports text, images, context, tool schemas, and response format specifications.
|
311
|
+
"""
|
667
312
|
|
668
|
-
|
313
|
+
role: MessageRole = MessageRole.USER
|
314
|
+
content: InstructionContent
|
315
|
+
|
316
|
+
@field_validator("content", mode="before")
|
317
|
+
def _validate_content(cls, v):
|
318
|
+
if v is None:
|
319
|
+
return InstructionContent()
|
320
|
+
if isinstance(v, dict):
|
321
|
+
return InstructionContent.from_dict(v)
|
322
|
+
if isinstance(v, InstructionContent):
|
323
|
+
return v
|
324
|
+
raise TypeError("content must be dict or InstructionContent instance")
|