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.
Files changed (52) hide show
  1. lionagi/libs/schema/minimal_yaml.py +98 -0
  2. lionagi/ln/types.py +32 -5
  3. lionagi/models/field_model.py +9 -0
  4. lionagi/operations/ReAct/ReAct.py +474 -237
  5. lionagi/operations/ReAct/utils.py +3 -0
  6. lionagi/operations/act/act.py +206 -0
  7. lionagi/operations/chat/chat.py +130 -114
  8. lionagi/operations/communicate/communicate.py +101 -42
  9. lionagi/operations/flow.py +4 -4
  10. lionagi/operations/interpret/interpret.py +65 -20
  11. lionagi/operations/operate/operate.py +212 -106
  12. lionagi/operations/parse/parse.py +170 -142
  13. lionagi/operations/select/select.py +78 -17
  14. lionagi/operations/select/utils.py +1 -1
  15. lionagi/operations/types.py +119 -23
  16. lionagi/protocols/generic/log.py +3 -2
  17. lionagi/protocols/messages/__init__.py +27 -0
  18. lionagi/protocols/messages/action_request.py +86 -184
  19. lionagi/protocols/messages/action_response.py +73 -131
  20. lionagi/protocols/messages/assistant_response.py +130 -159
  21. lionagi/protocols/messages/base.py +26 -18
  22. lionagi/protocols/messages/instruction.py +281 -625
  23. lionagi/protocols/messages/manager.py +112 -62
  24. lionagi/protocols/messages/message.py +87 -197
  25. lionagi/protocols/messages/system.py +52 -123
  26. lionagi/protocols/types.py +0 -2
  27. lionagi/service/connections/endpoint.py +0 -8
  28. lionagi/service/connections/providers/oai_.py +29 -94
  29. lionagi/service/connections/providers/ollama_.py +3 -2
  30. lionagi/service/hooks/hooked_event.py +2 -2
  31. lionagi/service/third_party/claude_code.py +3 -2
  32. lionagi/service/third_party/openai_models.py +433 -0
  33. lionagi/session/branch.py +170 -178
  34. lionagi/session/session.py +3 -9
  35. lionagi/tools/file/reader.py +2 -2
  36. lionagi/version.py +1 -1
  37. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/METADATA +1 -2
  38. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/RECORD +41 -49
  39. lionagi/operations/_act/act.py +0 -86
  40. lionagi/protocols/messages/templates/README.md +0 -28
  41. lionagi/protocols/messages/templates/action_request.jinja2 +0 -5
  42. lionagi/protocols/messages/templates/action_response.jinja2 +0 -9
  43. lionagi/protocols/messages/templates/assistant_response.jinja2 +0 -6
  44. lionagi/protocols/messages/templates/instruction_message.jinja2 +0 -61
  45. lionagi/protocols/messages/templates/system_message.jinja2 +0 -11
  46. lionagi/protocols/messages/templates/tool_schemas.jinja2 +0 -7
  47. lionagi/service/connections/providers/types.py +0 -28
  48. lionagi/service/third_party/openai_model_names.py +0 -198
  49. lionagi/service/types.py +0 -58
  50. /lionagi/operations/{_act → act}/__init__.py +0 -0
  51. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/WHEEL +0 -0
  52. {lionagi-0.17.11.dist-info → lionagi-0.18.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,668 +1,324 @@
1
- # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
3
-
1
+ import inspect
2
+ from dataclasses import dataclass, field
4
3
  from typing import Any, Literal
5
4
 
6
- from pydantic import BaseModel, JsonValue, field_serializer
7
- from typing_extensions import override
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
- Emphasizes brevity and clarity:
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
- # Join all lines into a single string
122
- return "\n".join(lines).strip()
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
- def _render_value(val) -> str:
126
- """
127
- Render an arbitrary value (scalar, list, dict) in minimal form:
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
- def _render_dict(dct: dict) -> str:
141
- """
142
- Minimal bullet list for dictionary items:
143
- key: rendered subvalue
144
- """
145
- lines = []
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
- if request_fields and request_model:
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
- out_ = {"context": []}
268
- if guidance:
269
- out_["guidance"] = guidance
270
- if instruction:
271
- out_["instruction"] = instruction
272
- if context:
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
- if plain_content:
299
- out_["plain_content"] = plain_content
300
-
301
- # remove keys with None/UNDEFINED
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
- return cls(
387
- role=MessageRole.USER,
388
- content=content,
389
- sender=sender,
390
- recipient=recipient,
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 guidance(self) -> str | None:
395
- return self.content.get("guidance", None)
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 plain_content(self) -> str | None:
443
- return self.content.get("plain_content", None)
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 images(self) -> list:
459
- return self.content.get("images", [])
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 request_fields(self) -> dict | None:
467
- return self.content.get("request_fields", None)
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 response_format(self) -> type[BaseModel] | None:
478
- return self.content.get("request_model", None)
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
- @response_format.setter
481
- def response_format(self, model: type[BaseModel]) -> None:
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
- if isinstance(model, BaseModel):
487
- self.content["request_model"] = type(model)
488
- else:
489
- self.content["request_model"] = model
155
+ inst = cls()
490
156
 
491
- self.request_fields = {}
492
- self.extend_context(respond_schema_info=model.model_json_schema())
493
- self.request_fields = breakdown_pydantic_annotation(model)
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
- @property
496
- def respond_schema_info(self) -> dict | None:
497
- return self.content.get("respond_schema_info", None)
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
- "You cannot pass both request_model and request_fields to create_instruction"
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
- if image_detail:
618
- self.image_detail = image_detail
619
-
620
- if tool_schemas:
621
- self.tool_schemas = tool_schemas
622
-
623
- if sender:
624
- self.sender = sender
625
-
626
- if recipient:
627
- self.recipient = recipient
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 context:
630
- self.extend_context(context)
185
+ if ts := data.get("tool_schemas"):
186
+ inst.tool_schemas.extend(ts if isinstance(ts, list) else [ts])
631
187
 
632
- @override
633
- @property
634
- def rendered(self) -> Any:
635
- """
636
- Convert content into a text or combined text+image structure.
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
- @field_serializer("content")
655
- def _serialize_content(self, values) -> dict:
656
- """
657
- Remove certain ephemeral fields before saving.
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
- return values
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
- # File: lionagi/protocols/messages/instruction.py
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")