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,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
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
27
|
-
if isinstance(
|
28
|
-
|
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(
|
37
|
+
model_responses.append(item)
|
31
38
|
|
32
|
-
if isinstance(
|
33
|
-
#
|
34
|
-
if "content" in
|
35
|
-
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
|
40
|
-
if isinstance(
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
53
|
-
if "message" in
|
54
|
-
text_contents.append(
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
63
|
-
if isinstance(
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
102
|
-
|
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
|
-
"""
|
108
|
-
return
|
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
|
-
|
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
|
155
|
+
def from_response(
|
126
156
|
cls,
|
127
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
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
|
169
|
+
AssistantResponse with parsed content and metadata
|
151
170
|
"""
|
152
|
-
|
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
|
-
|
182
|
-
assistant_response
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
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
|
-
|
94
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|