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,207 +1,178 @@
1
1
  # Copyright (c) 2023-2025, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ from dataclasses import dataclass
4
5
  from typing import Any
5
6
 
6
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, field_validator
7
8
 
8
- from lionagi.utils import copy
9
+ from .base import SenderRecipient
10
+ from .message import MessageContent, MessageRole, RoledMessage
9
11
 
10
- from .base import MessageRole, SenderRecipient
11
- from .message import MessageRole, RoledMessage, Template, jinja_env
12
12
 
13
+ def parse_assistant_response(
14
+ response: BaseModel | list[BaseModel] | dict | str | Any,
15
+ ) -> tuple[str, dict | list[dict]]:
16
+ """Parse various AI model response formats into text and raw data.
13
17
 
14
- def prepare_assistant_response(
15
- assistant_response: BaseModel | list[BaseModel] | dict | str | Any, /
16
- ) -> dict:
17
- assistant_response = (
18
- [assistant_response]
19
- if not isinstance(assistant_response, list)
20
- else assistant_response
21
- )
18
+ Supports:
19
+ - Anthropic format (content field)
20
+ - OpenAI chat completions (choices field)
21
+ - OpenAI responses API (output field)
22
+ - Claude Code (result field)
23
+ - Raw strings
24
+
25
+ Returns:
26
+ tuple: (extracted_text, raw_model_response)
27
+ """
28
+ responses = [response] if not isinstance(response, list) else response
22
29
 
23
30
  text_contents = []
24
31
  model_responses = []
25
32
 
26
- for i in assistant_response:
27
- if isinstance(i, BaseModel):
28
- i = i.model_dump(exclude_none=True, exclude_unset=True)
33
+ for item in responses:
34
+ if isinstance(item, BaseModel):
35
+ item = item.model_dump(exclude_none=True, exclude_unset=True)
29
36
 
30
- model_responses.append(i)
37
+ model_responses.append(item)
31
38
 
32
- if isinstance(i, dict):
33
- # anthropic standard
34
- if "content" in i:
35
- content = i["content"]
39
+ if isinstance(item, dict):
40
+ # Anthropic standard
41
+ if "content" in item:
42
+ content = item["content"]
36
43
  content = (
37
44
  [content] if not isinstance(content, list) else content
38
45
  )
39
- for j in content:
40
- if isinstance(j, dict):
41
- if j.get("type") == "text":
42
- text_contents.append(j["text"])
43
- elif isinstance(j, str):
44
- text_contents.append(j)
45
-
46
- # openai chat completions standard
47
- elif "choices" in i:
48
- choices = i["choices"]
46
+ for c in content:
47
+ if isinstance(c, dict) and c.get("type") == "text":
48
+ text_contents.append(c["text"])
49
+ elif isinstance(c, str):
50
+ text_contents.append(c)
51
+
52
+ # OpenAI chat completions standard
53
+ elif "choices" in item:
54
+ choices = item["choices"]
49
55
  choices = (
50
56
  [choices] if not isinstance(choices, list) else choices
51
57
  )
52
- for j in choices:
53
- if "message" in j:
54
- text_contents.append(j["message"]["content"] or "")
55
- elif "delta" in j:
56
- text_contents.append(j["delta"]["content"] or "")
57
-
58
- # openai responses API standard
59
- elif "output" in i:
60
- output = i["output"]
58
+ for choice in choices:
59
+ if "message" in choice:
60
+ text_contents.append(
61
+ choice["message"].get("content") or ""
62
+ )
63
+ elif "delta" in choice:
64
+ text_contents.append(
65
+ choice["delta"].get("content") or ""
66
+ )
67
+
68
+ # OpenAI responses API standard
69
+ elif "output" in item:
70
+ output = item["output"]
61
71
  output = [output] if not isinstance(output, list) else output
62
- for item in output:
63
- if isinstance(item, dict):
64
- if item.get("type") == "message":
65
- # Extract content from message
66
- content = item.get("content", [])
67
- if isinstance(content, list):
68
- for c in content:
69
- if (
70
- isinstance(c, dict)
71
- and c.get("type") == "output_text"
72
- ):
73
- text_contents.append(c.get("text", ""))
74
- elif isinstance(c, str):
75
- text_contents.append(c)
76
-
77
- # claude code standard
78
- elif "result" in i:
79
- text_contents.append(i["result"])
80
-
81
- elif isinstance(i, str):
82
- text_contents.append(i)
83
-
84
- text_contents = "".join(text_contents)
85
- model_responses = (
72
+ for out in output:
73
+ if isinstance(out, dict) and out.get("type") == "message":
74
+ content = out.get("content", [])
75
+ if isinstance(content, list):
76
+ for c in content:
77
+ if (
78
+ isinstance(c, dict)
79
+ and c.get("type") == "output_text"
80
+ ):
81
+ text_contents.append(c.get("text", ""))
82
+ elif isinstance(c, str):
83
+ text_contents.append(c)
84
+
85
+ # Claude Code standard
86
+ elif "result" in item:
87
+ text_contents.append(item["result"])
88
+
89
+ elif isinstance(item, str):
90
+ text_contents.append(item)
91
+
92
+ text = "".join(text_contents)
93
+ model_response = (
86
94
  model_responses[0] if len(model_responses) == 1 else model_responses
87
95
  )
88
- return {
89
- "assistant_response": text_contents,
90
- "model_response": model_responses,
91
- }
92
96
 
97
+ return text, model_response
93
98
 
94
- class AssistantResponse(RoledMessage):
99
+
100
+ @dataclass(slots=True)
101
+ class AssistantResponseContent(MessageContent):
102
+ """Content for assistant responses.
103
+
104
+ Fields:
105
+ assistant_response: Extracted text from the model
95
106
  """
96
- A message representing the AI assistant's reply, typically
97
- from a model or LLM call. If the raw model output is available,
98
- it's placed in `metadata["model_response"]`.
107
+
108
+ assistant_response: str = ""
109
+
110
+ @property
111
+ def rendered(self) -> str:
112
+ """Render assistant response as plain text."""
113
+ return self.assistant_response
114
+
115
+ @classmethod
116
+ def from_dict(cls, data: dict[str, Any]) -> "AssistantResponseContent":
117
+ """Construct AssistantResponseContent from dictionary."""
118
+ assistant_response = data.get("assistant_response", "")
119
+ return cls(assistant_response=assistant_response)
120
+
121
+
122
+ class AssistantResponse(RoledMessage):
123
+ """Message representing an AI assistant's reply.
124
+
125
+ The raw model output is stored in metadata["model_response"].
99
126
  """
100
127
 
101
- template: Template | str | None = jinja_env.get_template(
102
- "assistant_response.jinja2"
103
- )
128
+ role: MessageRole = MessageRole.ASSISTANT
129
+ content: AssistantResponseContent
130
+ recipient: SenderRecipient | None = MessageRole.USER
131
+
132
+ @field_validator("content", mode="before")
133
+ def _validate_content(cls, v):
134
+ if v is None:
135
+ return AssistantResponseContent()
136
+ if isinstance(v, dict):
137
+ return AssistantResponseContent.from_dict(v)
138
+ if isinstance(v, AssistantResponseContent):
139
+ return v
140
+ raise TypeError(
141
+ "content must be dict or AssistantResponseContent instance"
142
+ )
104
143
 
105
144
  @property
106
145
  def response(self) -> str:
107
- """Get or set the text portion of the assistant's response."""
108
- return copy(self.content["assistant_response"])
109
-
110
- @response.setter
111
- def response(self, value: str) -> None:
112
- self.content["assistant_response"] = value
146
+ """Access the text response from the assistant."""
147
+ return self.content.assistant_response
113
148
 
114
149
  @property
115
150
  def model_response(self) -> dict | list[dict]:
116
- """
117
- Access the underlying model's raw data, if available.
118
-
119
- Returns:
120
- dict or list[dict]: The stored model output data.
121
- """
122
- return copy(self.metadata.get("model_response", {}))
151
+ """Access the underlying model's raw data from metadata."""
152
+ return self.metadata.get("model_response", {})
123
153
 
124
154
  @classmethod
125
- def create(
155
+ def from_response(
126
156
  cls,
127
- assistant_response: BaseModel | list[BaseModel] | dict | str | Any,
157
+ response: BaseModel | list[BaseModel] | dict | str | Any,
128
158
  sender: SenderRecipient | None = None,
129
159
  recipient: SenderRecipient | None = None,
130
- template: Template | str | None = None,
131
- **kwargs,
132
160
  ) -> "AssistantResponse":
133
- """
134
- Build an AssistantResponse from arbitrary assistant data.
161
+ """Create AssistantResponse from raw model output.
135
162
 
136
163
  Args:
137
- assistant_response:
138
- A pydantic model, list, dict, or string representing
139
- an LLM or system response.
140
- sender (SenderRecipient | None):
141
- The ID or role denoting who sends this response.
142
- recipient (SenderRecipient | None):
143
- The ID or role to receive it.
144
- template (Template | str | None):
145
- Optional custom template.
146
- **kwargs:
147
- Additional content key-value pairs.
164
+ response: Raw model output in any supported format
165
+ sender: Message sender
166
+ recipient: Message recipient
148
167
 
149
168
  Returns:
150
- AssistantResponse: The constructed instance.
169
+ AssistantResponse with parsed content and metadata
151
170
  """
152
- content = prepare_assistant_response(assistant_response)
153
- model_response = content.pop("model_response", {})
154
- content.update(kwargs)
155
- params = {
156
- "content": content,
157
- "role": MessageRole.ASSISTANT,
158
- "recipient": recipient or MessageRole.USER,
159
- }
160
- if sender:
161
- params["sender"] = sender
162
- if template:
163
- params["template"] = template
164
- if model_response:
165
- params["metadata"] = {"model_response": model_response}
166
- return cls(**params)
167
-
168
- def update(
169
- self,
170
- assistant_response: (
171
- BaseModel | list[BaseModel] | dict | str | Any
172
- ) = None,
173
- sender: SenderRecipient | None = None,
174
- recipient: SenderRecipient | None = None,
175
- template: Template | str | None = None,
176
- **kwargs,
177
- ):
178
- """
179
- Update this AssistantResponse with new data or fields.
171
+ text, model_response = parse_assistant_response(response)
180
172
 
181
- Args:
182
- assistant_response:
183
- Additional or replaced assistant model output.
184
- sender (SenderRecipient | None):
185
- Updated sender.
186
- recipient (SenderRecipient | None):
187
- Updated recipient.
188
- template (Template | str | None):
189
- Optional new template.
190
- **kwargs:
191
- Additional content updates for `self.content`.
192
- """
193
- if assistant_response:
194
- content = prepare_assistant_response(assistant_response)
195
- self.content.update(content)
196
- super().update(
197
- sender=sender, recipient=recipient, template=template, **kwargs
173
+ return cls(
174
+ content=AssistantResponseContent(assistant_response=text),
175
+ sender=sender,
176
+ recipient=recipient or MessageRole.USER,
177
+ metadata={"model_response": model_response},
198
178
  )
199
-
200
- def as_context(self) -> dict:
201
- return f"""
202
- Response: {self.response or "Not available"}
203
- Summary: {self.model_response.get("summary") or "Not available"}
204
- """.strip()
205
-
206
-
207
- # File: lionagi/protocols/messages/assistant_response.py
@@ -4,14 +4,14 @@
4
4
  from enum import Enum
5
5
  from typing import Any, TypeAlias
6
6
 
7
- from ..generic.element import ID, IDError, IDType, Observable
7
+ from ..generic.element import ID, Element, IDError, IDType, Observable
8
8
 
9
9
  __all__ = (
10
10
  "MessageRole",
11
- "MessageFlag",
12
11
  "MessageField",
13
12
  "MESSAGE_FIELDS",
14
13
  "validate_sender_recipient",
14
+ "serialize_sender_recipient",
15
15
  )
16
16
 
17
17
 
@@ -27,15 +27,6 @@ class MessageRole(str, Enum):
27
27
  ACTION = "action"
28
28
 
29
29
 
30
- class MessageFlag(str, Enum):
31
- """
32
- Internal flags for certain message states, e.g., clones or loads.
33
- """
34
-
35
- MESSAGE_CLONE = "MESSAGE_CLONE"
36
- MESSAGE_LOAD = "MESSAGE_LOAD"
37
-
38
-
39
30
  SenderRecipient: TypeAlias = IDType | MessageRole | str
40
31
  """
41
32
  A union type indicating that a sender or recipient could be:
@@ -75,7 +66,7 @@ def validate_sender_recipient(value: Any, /) -> SenderRecipient:
75
66
  Raises:
76
67
  ValueError: If the input cannot be recognized as a role or ID.
77
68
  """
78
- if isinstance(value, MessageRole | MessageFlag):
69
+ if isinstance(value, MessageRole):
79
70
  return value
80
71
 
81
72
  if isinstance(value, IDType):
@@ -90,13 +81,30 @@ def validate_sender_recipient(value: Any, /) -> SenderRecipient:
90
81
  if value in ["system", "user", "unset", "assistant", "action"]:
91
82
  return MessageRole(value)
92
83
 
93
- if value in ["MESSAGE_CLONE", "MESSAGE_LOAD"]:
94
- return MessageFlag(value)
84
+ # Accept plain strings (user names, identifiers, etc)
85
+ if isinstance(value, str):
86
+ # Try to parse as ID first, but allow plain strings as fallback
87
+ try:
88
+ return ID.get_id(value)
89
+ except IDError:
90
+ return value
95
91
 
96
- try:
97
- return ID.get_id(value)
98
- except IDError as e:
99
- raise ValueError("Invalid sender or recipient") from e
92
+ raise ValueError("Invalid sender or recipient")
93
+
94
+
95
+ def serialize_sender_recipient(value: Any) -> str | None:
96
+ if not value:
97
+ return None
98
+ # Check instance types first before enum membership
99
+ if isinstance(value, Element):
100
+ return str(value.id)
101
+ if isinstance(value, IDType):
102
+ return str(value)
103
+ if isinstance(value, MessageRole):
104
+ return value.value
105
+ if isinstance(value, str):
106
+ return value
107
+ return str(value)
100
108
 
101
109
 
102
110
  # File: lionagi/protocols/messages/base.py