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