lionagi 0.12.3__py3-none-any.whl → 0.12.4__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/config.py +123 -0
- lionagi/libs/schema/load_pydantic_model_from_schema.py +259 -0
- lionagi/libs/token_transform/perplexity.py +2 -4
- lionagi/libs/token_transform/synthlang_/translate_to_synthlang.py +1 -1
- lionagi/operations/chat/chat.py +2 -2
- lionagi/operations/communicate/communicate.py +20 -5
- lionagi/operations/parse/parse.py +131 -43
- lionagi/protocols/generic/pile.py +18 -4
- lionagi/protocols/messages/assistant_response.py +20 -1
- lionagi/service/connections/__init__.py +15 -0
- lionagi/service/connections/api_calling.py +230 -0
- lionagi/service/connections/endpoint.py +410 -0
- lionagi/service/connections/endpoint_config.py +137 -0
- lionagi/service/connections/header_factory.py +56 -0
- lionagi/service/connections/match_endpoint.py +49 -0
- lionagi/service/connections/providers/__init__.py +3 -0
- lionagi/service/connections/providers/anthropic_.py +87 -0
- lionagi/service/connections/providers/exa_.py +33 -0
- lionagi/service/connections/providers/oai_.py +166 -0
- lionagi/service/connections/providers/ollama_.py +122 -0
- lionagi/service/connections/providers/perplexity_.py +29 -0
- lionagi/service/imodel.py +36 -144
- lionagi/service/manager.py +1 -7
- lionagi/service/{endpoints/rate_limited_processor.py → rate_limited_processor.py} +4 -2
- lionagi/service/resilience.py +545 -0
- lionagi/service/third_party/README.md +71 -0
- lionagi/service/third_party/__init__.py +0 -0
- lionagi/service/third_party/anthropic_models.py +159 -0
- lionagi/service/{providers/exa_/models.py → third_party/exa_models.py} +18 -13
- lionagi/service/third_party/openai_models.py +18241 -0
- lionagi/service/third_party/pplx_models.py +156 -0
- lionagi/service/types.py +5 -4
- lionagi/session/branch.py +12 -7
- lionagi/tools/file/reader.py +1 -1
- lionagi/tools/memory/tools.py +497 -0
- lionagi/version.py +1 -1
- {lionagi-0.12.3.dist-info → lionagi-0.12.4.dist-info}/METADATA +16 -17
- {lionagi-0.12.3.dist-info → lionagi-0.12.4.dist-info}/RECORD +42 -43
- lionagi/service/endpoints/__init__.py +0 -3
- lionagi/service/endpoints/base.py +0 -706
- lionagi/service/endpoints/chat_completion.py +0 -116
- lionagi/service/endpoints/match_endpoint.py +0 -72
- lionagi/service/providers/__init__.py +0 -3
- lionagi/service/providers/anthropic_/__init__.py +0 -3
- lionagi/service/providers/anthropic_/messages.py +0 -99
- lionagi/service/providers/exa_/search.py +0 -80
- lionagi/service/providers/exa_/types.py +0 -7
- lionagi/service/providers/groq_/__init__.py +0 -3
- lionagi/service/providers/groq_/chat_completions.py +0 -56
- lionagi/service/providers/ollama_/__init__.py +0 -3
- lionagi/service/providers/ollama_/chat_completions.py +0 -134
- lionagi/service/providers/openai_/__init__.py +0 -3
- lionagi/service/providers/openai_/chat_completions.py +0 -101
- lionagi/service/providers/openai_/spec.py +0 -14
- lionagi/service/providers/openrouter_/__init__.py +0 -3
- lionagi/service/providers/openrouter_/chat_completions.py +0 -62
- lionagi/service/providers/perplexity_/__init__.py +0 -3
- lionagi/service/providers/perplexity_/chat_completions.py +0 -44
- lionagi/service/providers/perplexity_/models.py +0 -144
- lionagi/service/providers/types.py +0 -17
- /lionagi/{service/providers/exa_/__init__.py → py.typed} +0 -0
- /lionagi/service/{endpoints/token_calculator.py → token_calculator.py} +0 -0
- {lionagi-0.12.3.dist-info → lionagi-0.12.4.dist-info}/WHEEL +0 -0
- {lionagi-0.12.3.dist-info → lionagi-0.12.4.dist-info}/licenses/LICENSE +0 -0
lionagi/config.py
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
from typing import Any, ClassVar
|
6
|
+
|
7
|
+
from pydantic import BaseModel, Field, SecretStr
|
8
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
9
|
+
|
10
|
+
|
11
|
+
class CacheConfig(BaseModel):
|
12
|
+
"""Configuration for aiocache."""
|
13
|
+
|
14
|
+
ttl: int = 300
|
15
|
+
key: str | None = None
|
16
|
+
namespace: str | None = None
|
17
|
+
key_builder: Any = None
|
18
|
+
skip_cache_func: Any = lambda _: False
|
19
|
+
serializer: dict[str, Any] | None = None
|
20
|
+
plugins: Any = None
|
21
|
+
alias: str | None = None
|
22
|
+
noself: Any = lambda _: False
|
23
|
+
|
24
|
+
def as_kwargs(self) -> dict[str, Any]:
|
25
|
+
"""Convert config to kwargs dict for @cached decorator.
|
26
|
+
|
27
|
+
Removes unserialisable callables that aiocache can't pickle.
|
28
|
+
"""
|
29
|
+
raw = self.model_dump(exclude_none=True)
|
30
|
+
# Remove all unserialisable callables
|
31
|
+
unserialisable_keys = (
|
32
|
+
"key_builder",
|
33
|
+
"skip_cache_func",
|
34
|
+
"noself",
|
35
|
+
"serializer",
|
36
|
+
"plugins",
|
37
|
+
)
|
38
|
+
for key in unserialisable_keys:
|
39
|
+
raw.pop(key, None)
|
40
|
+
return raw
|
41
|
+
|
42
|
+
|
43
|
+
class AppSettings(BaseSettings, frozen=True):
|
44
|
+
"""Application settings with environment variable support."""
|
45
|
+
|
46
|
+
model_config = SettingsConfigDict(
|
47
|
+
env_file=(".env", ".env.local", ".secrets.env"),
|
48
|
+
env_file_encoding="utf-8",
|
49
|
+
case_sensitive=False,
|
50
|
+
extra="ignore",
|
51
|
+
)
|
52
|
+
|
53
|
+
aiocache_config: CacheConfig = Field(
|
54
|
+
default_factory=CacheConfig, description="Cache settings for aiocache"
|
55
|
+
)
|
56
|
+
|
57
|
+
# secrets
|
58
|
+
OPENAI_API_KEY: SecretStr | None = None
|
59
|
+
OPENROUTER_API_KEY: SecretStr | None = None
|
60
|
+
OLLAMA_API_KEY: SecretStr | None = None
|
61
|
+
EXA_API_KEY: SecretStr | None = None
|
62
|
+
PERPLEXITY_API_KEY: SecretStr | None = None
|
63
|
+
GROQ_API_KEY: SecretStr | None = None
|
64
|
+
ANTHROPIC_API_KEY: SecretStr | None = None
|
65
|
+
|
66
|
+
# defaults models
|
67
|
+
LIONAGI_EMBEDDING_PROVIDER: str = "openai"
|
68
|
+
LIONAGI_EMBEDDING_MODEL: str = "text-embedding-3-small"
|
69
|
+
|
70
|
+
LIONAGI_CHAT_PROVIDER: str = "openai"
|
71
|
+
LIONAGI_CHAT_MODEL: str = "gpt-4o"
|
72
|
+
|
73
|
+
# default storage
|
74
|
+
LIONAGI_AUTO_STORE_EVENT: bool = False
|
75
|
+
LIONAGI_STORAGE_PROVIDER: str = "async_qdrant"
|
76
|
+
|
77
|
+
LIONAGI_AUTO_EMBED_LOG: bool = False
|
78
|
+
|
79
|
+
# specific storage
|
80
|
+
LIONAGI_QDRANT_URL: str = "http://localhost:6333"
|
81
|
+
LIONAGI_DEFAULT_QDRANT_COLLECTION: str = "event_logs"
|
82
|
+
|
83
|
+
# Class variable to store the singleton instance
|
84
|
+
_instance: ClassVar[Any] = None
|
85
|
+
|
86
|
+
def get_secret(self, key_name: str) -> str:
|
87
|
+
"""
|
88
|
+
Get the secret value for a given key name.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
key_name: The name of the secret key (e.g., "OPENAI_API_KEY")
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
The secret value as a string
|
95
|
+
|
96
|
+
Raises:
|
97
|
+
AttributeError: If the key doesn't exist
|
98
|
+
ValueError: If the key exists but is None
|
99
|
+
"""
|
100
|
+
if not hasattr(self, key_name):
|
101
|
+
if "ollama" in key_name.lower():
|
102
|
+
return "ollama"
|
103
|
+
raise AttributeError(
|
104
|
+
f"Secret key '{key_name}' not found in settings"
|
105
|
+
)
|
106
|
+
|
107
|
+
secret = getattr(self, key_name)
|
108
|
+
if secret is None:
|
109
|
+
# Special case for Ollama - return "ollama" even if key exists but is None
|
110
|
+
if "ollama" in key_name.lower():
|
111
|
+
return "ollama"
|
112
|
+
raise ValueError(f"Secret key '{key_name}' is not set")
|
113
|
+
|
114
|
+
if isinstance(secret, SecretStr):
|
115
|
+
return secret.get_secret_value()
|
116
|
+
|
117
|
+
return str(secret)
|
118
|
+
|
119
|
+
|
120
|
+
# Create a singleton instance
|
121
|
+
settings = AppSettings()
|
122
|
+
# Store the instance in the class variable for singleton pattern
|
123
|
+
AppSettings._instance = settings
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
|
5
|
+
import importlib.util
|
6
|
+
import json
|
7
|
+
import string
|
8
|
+
import tempfile
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, TypeVar
|
11
|
+
|
12
|
+
from pydantic import BaseModel, PydanticUserError
|
13
|
+
|
14
|
+
from lionagi.utils import is_import_installed
|
15
|
+
|
16
|
+
_HAS_DATAMODEL_CODE_GENERATOR = is_import_installed("datamodel-code-generator")
|
17
|
+
|
18
|
+
# Import at module level for easier mocking in tests
|
19
|
+
if _HAS_DATAMODEL_CODE_GENERATOR:
|
20
|
+
from datamodel_code_generator import (
|
21
|
+
DataModelType,
|
22
|
+
InputFileType,
|
23
|
+
PythonVersion,
|
24
|
+
generate,
|
25
|
+
)
|
26
|
+
else:
|
27
|
+
# Create dummy objects for when package is not installed
|
28
|
+
DataModelType = None
|
29
|
+
InputFileType = None
|
30
|
+
PythonVersion = None
|
31
|
+
generate = None
|
32
|
+
|
33
|
+
|
34
|
+
B = TypeVar("B", bound=BaseModel)
|
35
|
+
|
36
|
+
|
37
|
+
def load_pydantic_model_from_schema(
|
38
|
+
schema: str | dict[str, Any],
|
39
|
+
model_name: str = "DynamicModel",
|
40
|
+
/,
|
41
|
+
pydantic_version=None,
|
42
|
+
python_version=None,
|
43
|
+
) -> type[BaseModel]:
|
44
|
+
"""
|
45
|
+
Generates a Pydantic model class dynamically from a JSON schema string or dict,
|
46
|
+
and ensures it's fully resolved using model_rebuild() with the correct namespace.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
schema: The JSON schema as a string or a Python dictionary.
|
50
|
+
model_name: The desired base name for the generated Pydantic model.
|
51
|
+
If the schema has a 'title', that will likely be used.
|
52
|
+
pydantic_version: The Pydantic model type to generate.
|
53
|
+
python_version: The target Python version for generated code syntax.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
The dynamically created and resolved Pydantic BaseModel class.
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
ValueError: If the schema is invalid.
|
60
|
+
FileNotFoundError: If the generated model file is not found.
|
61
|
+
AttributeError: If the expected model class cannot be found.
|
62
|
+
RuntimeError: For errors during generation, loading, or rebuilding.
|
63
|
+
Exception: For other potential errors.
|
64
|
+
"""
|
65
|
+
if not _HAS_DATAMODEL_CODE_GENERATOR:
|
66
|
+
error_msg = "`datamodel-code-generator` is not installed. Please install with `pip install datamodel-code-generator`."
|
67
|
+
raise ImportError(error_msg)
|
68
|
+
|
69
|
+
if DataModelType is not None:
|
70
|
+
pydantic_version = (
|
71
|
+
pydantic_version or DataModelType.PydanticV2BaseModel
|
72
|
+
)
|
73
|
+
python_version = python_version or PythonVersion.PY_312
|
74
|
+
else:
|
75
|
+
# These won't be used since we'll raise ImportError above
|
76
|
+
pydantic_version = None
|
77
|
+
python_version = None
|
78
|
+
|
79
|
+
schema_input_data: str
|
80
|
+
schema_dict: dict[str, Any]
|
81
|
+
resolved_model_name = (
|
82
|
+
model_name # Keep track of the potentially updated name
|
83
|
+
)
|
84
|
+
|
85
|
+
# --- 1. Prepare Schema Input ---
|
86
|
+
if isinstance(schema, dict):
|
87
|
+
try:
|
88
|
+
model_name_from_title = schema.get("title")
|
89
|
+
if model_name_from_title and isinstance(
|
90
|
+
model_name_from_title, str
|
91
|
+
):
|
92
|
+
valid_chars = string.ascii_letters + string.digits + "_"
|
93
|
+
sanitized_title = "".join(
|
94
|
+
c
|
95
|
+
for c in model_name_from_title.replace(" ", "")
|
96
|
+
if c in valid_chars
|
97
|
+
)
|
98
|
+
if sanitized_title and sanitized_title[0].isalpha():
|
99
|
+
resolved_model_name = (
|
100
|
+
sanitized_title # Update the name to use
|
101
|
+
)
|
102
|
+
schema_dict = schema
|
103
|
+
schema_input_data = json.dumps(schema)
|
104
|
+
except TypeError as e:
|
105
|
+
error_msg = "Invalid dictionary provided for schema"
|
106
|
+
raise ValueError(error_msg) from e
|
107
|
+
elif isinstance(schema, str):
|
108
|
+
try:
|
109
|
+
schema_dict = json.loads(schema)
|
110
|
+
model_name_from_title = schema_dict.get("title")
|
111
|
+
if model_name_from_title and isinstance(
|
112
|
+
model_name_from_title, str
|
113
|
+
):
|
114
|
+
valid_chars = string.ascii_letters + string.digits + "_"
|
115
|
+
sanitized_title = "".join(
|
116
|
+
c
|
117
|
+
for c in model_name_from_title.replace(" ", "")
|
118
|
+
if c in valid_chars
|
119
|
+
)
|
120
|
+
if sanitized_title and sanitized_title[0].isalpha():
|
121
|
+
resolved_model_name = (
|
122
|
+
sanitized_title # Update the name to use
|
123
|
+
)
|
124
|
+
schema_input_data = schema
|
125
|
+
except json.JSONDecodeError as e:
|
126
|
+
error_msg = "Invalid JSON schema string provided"
|
127
|
+
raise ValueError(error_msg) from e
|
128
|
+
else:
|
129
|
+
error_msg = "Schema must be a JSON string or a dictionary."
|
130
|
+
raise TypeError(error_msg)
|
131
|
+
|
132
|
+
# --- 2. Generate Code to Temporary File ---
|
133
|
+
with tempfile.TemporaryDirectory() as temporary_directory_name:
|
134
|
+
temporary_directory = Path(temporary_directory_name)
|
135
|
+
# Use a predictable but unique-ish filename
|
136
|
+
output_file = (
|
137
|
+
temporary_directory
|
138
|
+
/ f"{resolved_model_name.lower()}_model_{hash(schema_input_data)}.py"
|
139
|
+
)
|
140
|
+
module_name = output_file.stem # e.g., "userprofile_model_12345"
|
141
|
+
|
142
|
+
try:
|
143
|
+
generate(
|
144
|
+
schema_input_data,
|
145
|
+
input_file_type=InputFileType.JsonSchema,
|
146
|
+
input_filename="schema.json",
|
147
|
+
output=output_file,
|
148
|
+
output_model_type=pydantic_version,
|
149
|
+
target_python_version=python_version,
|
150
|
+
# Ensure necessary base models are imported in the generated code
|
151
|
+
base_class="pydantic.BaseModel",
|
152
|
+
)
|
153
|
+
except Exception as e:
|
154
|
+
# Optional: Print generated code on failure for debugging
|
155
|
+
# if output_file.exists():
|
156
|
+
# print(f"--- Generated Code (Error) ---\n{output_file.read_text()}\n--------------------------")
|
157
|
+
error_msg = "Failed to generate model code"
|
158
|
+
raise RuntimeError(error_msg) from e
|
159
|
+
|
160
|
+
if not output_file.exists():
|
161
|
+
error_msg = f"Generated model file was not created: {output_file}"
|
162
|
+
raise FileNotFoundError(error_msg)
|
163
|
+
|
164
|
+
def get_modules():
|
165
|
+
spec = importlib.util.spec_from_file_location(
|
166
|
+
module_name, str(output_file)
|
167
|
+
)
|
168
|
+
|
169
|
+
if spec is None or spec.loader is None:
|
170
|
+
error_msg = f"Could not create module spec for {output_file}"
|
171
|
+
raise ImportError(error_msg)
|
172
|
+
|
173
|
+
return spec, importlib.util.module_from_spec(spec)
|
174
|
+
|
175
|
+
# --- 3. Import the Generated Module Dynamically ---
|
176
|
+
try:
|
177
|
+
spec, generated_module = get_modules()
|
178
|
+
# Important: Make pydantic available within the executed module's globals
|
179
|
+
# if it's not explicitly imported by the generated code for some reason.
|
180
|
+
# Usually, datamodel-code-generator handles imports well.
|
181
|
+
# generated_module.__dict__['BaseModel'] = BaseModel
|
182
|
+
spec.loader.exec_module(generated_module)
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
# Optional: Print generated code on failure for debugging
|
186
|
+
# print(f"--- Generated Code (Import Error) ---\n{output_file.read_text()}\n--------------------------")
|
187
|
+
error_msg = f"Failed to load generated module ({output_file})"
|
188
|
+
raise RuntimeError(error_msg) from e
|
189
|
+
|
190
|
+
def validate_base_model_class(m):
|
191
|
+
if not isinstance(m, type) or not issubclass(m, BaseModel):
|
192
|
+
error_msg = f"Found attribute '{resolved_model_name}' is not a Pydantic BaseModel class."
|
193
|
+
raise TypeError(error_msg)
|
194
|
+
|
195
|
+
# --- 4. Find the Model Class ---
|
196
|
+
model_class: type[BaseModel]
|
197
|
+
try:
|
198
|
+
# Use the name potentially derived from the schema title
|
199
|
+
model_class = getattr(generated_module, resolved_model_name)
|
200
|
+
validate_base_model_class(model_class)
|
201
|
+
|
202
|
+
except AttributeError:
|
203
|
+
# Fallback attempt (less likely now with title extraction)
|
204
|
+
try:
|
205
|
+
model_class = generated_module.Model # Default fallback name
|
206
|
+
validate_base_model_class(model_class)
|
207
|
+
print(
|
208
|
+
f"Warning: Model name '{resolved_model_name}' not found, falling back to 'Model'."
|
209
|
+
)
|
210
|
+
except AttributeError as e:
|
211
|
+
# List available Pydantic models found in the module for debugging
|
212
|
+
available_attrs = [
|
213
|
+
attr
|
214
|
+
for attr in dir(generated_module)
|
215
|
+
if isinstance(getattr(generated_module, attr, None), type)
|
216
|
+
and issubclass(
|
217
|
+
getattr(generated_module, attr, object), BaseModel
|
218
|
+
) # Check inheritance safely
|
219
|
+
and getattr(generated_module, attr, None)
|
220
|
+
is not BaseModel # Exclude BaseModel itself
|
221
|
+
]
|
222
|
+
# Optional: Print generated code on failure for debugging
|
223
|
+
# print(f"--- Generated Code (AttributeError) ---\n{output_file.read_text()}\n--------------------------")
|
224
|
+
error_msg = (
|
225
|
+
f"Could not find expected model class '{resolved_model_name}' or fallback 'Model' "
|
226
|
+
f"in the generated module {output_file}. "
|
227
|
+
f"Found Pydantic models: {available_attrs}"
|
228
|
+
)
|
229
|
+
raise AttributeError(error_msg) from e
|
230
|
+
except TypeError as e:
|
231
|
+
error_msg = (
|
232
|
+
f"Error validating found model class '{resolved_model_name}'"
|
233
|
+
)
|
234
|
+
raise TypeError(error_msg) from e
|
235
|
+
|
236
|
+
# --- 5. Rebuild the Model (Providing Namespace) ---
|
237
|
+
try:
|
238
|
+
# Pass the generated module's dictionary as the namespace
|
239
|
+
# for resolving type hints like 'Status', 'ProfileDetails', etc.
|
240
|
+
model_class.model_rebuild(
|
241
|
+
_types_namespace=generated_module.__dict__,
|
242
|
+
force=True, # Force rebuild even if Pydantic thinks it's okay
|
243
|
+
)
|
244
|
+
except (
|
245
|
+
PydanticUserError,
|
246
|
+
NameError,
|
247
|
+
) as e: # Catch NameError explicitly here
|
248
|
+
# Optional: Print generated code on failure for debugging
|
249
|
+
# print(f"--- Generated Code (Rebuild Error) ---\n{output_file.read_text()}\n--------------------------")
|
250
|
+
error_msg = f"Error during model_rebuild for {resolved_model_name}"
|
251
|
+
raise RuntimeError(error_msg) from e
|
252
|
+
except Exception as e:
|
253
|
+
# Optional: Print generated code on failure for debugging
|
254
|
+
# print(f"--- Generated Code (Rebuild Error) ---\n{output_file.read_text()}\n--------------------------")
|
255
|
+
error_msg = f"Unexpected error during model_rebuild for {resolved_model_name}"
|
256
|
+
raise RuntimeError(error_msg) from e
|
257
|
+
|
258
|
+
# --- 6. Return the Resolved Model Class ---
|
259
|
+
return model_class
|
@@ -7,7 +7,7 @@ from pydantic import BaseModel
|
|
7
7
|
|
8
8
|
from lionagi.protocols.generic.event import EventStatus
|
9
9
|
from lionagi.protocols.generic.log import Log
|
10
|
-
from lionagi.service.
|
10
|
+
from lionagi.service.connections.api_calling import APICalling
|
11
11
|
from lionagi.service.imodel import iModel
|
12
12
|
from lionagi.utils import alcall, lcall, to_dict, to_list
|
13
13
|
|
@@ -208,9 +208,7 @@ class LLMCompressor:
|
|
208
208
|
Tokenize text. If no custom tokenizer, use the default from lionagi.
|
209
209
|
"""
|
210
210
|
if not self.tokenizer:
|
211
|
-
from lionagi.service.
|
212
|
-
TokenCalculator,
|
213
|
-
)
|
211
|
+
from lionagi.service.token_calculator import TokenCalculator
|
214
212
|
|
215
213
|
return TokenCalculator.tokenize(
|
216
214
|
text,
|
@@ -93,7 +93,7 @@ async def translate_to_synthlang(
|
|
93
93
|
else:
|
94
94
|
branch = Branch(system=final_prompt, chat_model=chat_model)
|
95
95
|
|
96
|
-
from lionagi.service.
|
96
|
+
from lionagi.service.token_calculator import TokenCalculator
|
97
97
|
|
98
98
|
calculator = TokenCalculator()
|
99
99
|
|
lionagi/operations/chat/chat.py
CHANGED
@@ -122,8 +122,8 @@ async def chat(
|
|
122
122
|
_msgs.append(i)
|
123
123
|
messages = _msgs
|
124
124
|
|
125
|
-
|
126
|
-
if branch.msgs.system
|
125
|
+
# All endpoints now assume sequential exchange (system message embedded in first user message)
|
126
|
+
if branch.msgs.system:
|
127
127
|
messages = [msg for msg in messages if msg.role != "system"]
|
128
128
|
first_instruction = None
|
129
129
|
|
@@ -91,12 +91,27 @@ async def communicate(
|
|
91
91
|
return res.response
|
92
92
|
|
93
93
|
if response_format is not None:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
max_retries=num_parse_retries,
|
94
|
+
# Default to raising errors unless explicitly set in fuzzy_match_kwargs
|
95
|
+
parse_kwargs = {
|
96
|
+
"handle_validation": "raise", # Default to raising errors
|
98
97
|
**(fuzzy_match_kwargs or {}),
|
99
|
-
|
98
|
+
}
|
99
|
+
|
100
|
+
try:
|
101
|
+
return await branch.parse(
|
102
|
+
text=res.response,
|
103
|
+
request_type=response_format,
|
104
|
+
max_retries=num_parse_retries,
|
105
|
+
**parse_kwargs,
|
106
|
+
)
|
107
|
+
except ValueError as e:
|
108
|
+
# Re-raise with more context
|
109
|
+
logging.error(
|
110
|
+
f"Failed to parse response '{res.response}' into {response_format}: {e}"
|
111
|
+
)
|
112
|
+
raise ValueError(
|
113
|
+
f"Failed to parse model response into {response_format.__name__}: {e}"
|
114
|
+
) from e
|
100
115
|
|
101
116
|
if request_fields is not None:
|
102
117
|
_d = fuzzy_validate_mapping(
|
@@ -35,35 +35,117 @@ async def parse(
|
|
35
35
|
suppress_conversion_errors: bool = False,
|
36
36
|
response_format=None,
|
37
37
|
):
|
38
|
-
_should_try = True
|
39
|
-
num_try = 0
|
40
|
-
response_model = text
|
41
38
|
if operative is not None:
|
42
39
|
max_retries = operative.max_retries
|
43
|
-
response_format = operative.request_type
|
40
|
+
response_format = operative.request_type or response_format
|
41
|
+
request_type = request_type or operative.request_type
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
43
|
+
if not request_type and not response_format:
|
44
|
+
raise ValueError(
|
45
|
+
"Either request_type or response_format must be provided"
|
46
|
+
)
|
47
|
+
|
48
|
+
request_type = request_type or response_format
|
49
|
+
|
50
|
+
# First attempt: try to parse the text directly
|
51
|
+
import logging
|
52
|
+
|
53
|
+
initial_error = None
|
54
|
+
parsed_data = None # Initialize to avoid scoping issues
|
55
|
+
|
56
|
+
try:
|
57
|
+
# Try fuzzy validation first
|
58
|
+
parsed_data = fuzzy_validate_mapping(
|
59
|
+
text,
|
60
|
+
breakdown_pydantic_annotation(request_type),
|
61
|
+
similarity_algo=similarity_algo,
|
62
|
+
similarity_threshold=similarity_threshold,
|
63
|
+
fuzzy_match=fuzzy_match,
|
64
|
+
handle_unmatched=handle_unmatched,
|
65
|
+
fill_value=fill_value,
|
66
|
+
fill_mapping=fill_mapping,
|
67
|
+
strict=strict,
|
68
|
+
suppress_conversion_errors=False, # Don't suppress on first attempt
|
62
69
|
)
|
70
|
+
|
71
|
+
logging.debug(f"Parsed data from fuzzy validation: {parsed_data}")
|
72
|
+
|
73
|
+
# Validate with pydantic
|
63
74
|
if operative is not None:
|
64
|
-
response_model = operative.update_response_model(
|
75
|
+
response_model = operative.update_response_model(parsed_data)
|
65
76
|
else:
|
66
|
-
response_model =
|
77
|
+
response_model = request_type.model_validate(parsed_data)
|
78
|
+
|
79
|
+
# If successful, return immediately
|
80
|
+
if isinstance(response_model, BaseModel):
|
81
|
+
return response_model
|
82
|
+
|
83
|
+
except Exception as e:
|
84
|
+
initial_error = e
|
85
|
+
# Log the initial parsing error for debugging
|
86
|
+
logging.debug(
|
87
|
+
f"Initial parsing failed for text '{text[:100]}...': {e}"
|
88
|
+
)
|
89
|
+
logging.debug(
|
90
|
+
f"Parsed data was: {locals().get('parsed_data', 'not set')}"
|
91
|
+
)
|
92
|
+
|
93
|
+
# Only continue if we have retries left
|
94
|
+
if max_retries <= 0:
|
95
|
+
if handle_validation == "raise":
|
96
|
+
raise ValueError(f"Failed to parse response: {e}") from e
|
97
|
+
elif handle_validation == "return_none":
|
98
|
+
return None
|
99
|
+
else: # return_value
|
100
|
+
return text
|
101
|
+
|
102
|
+
# If direct parsing failed, try using the parse model
|
103
|
+
num_try = 0
|
104
|
+
last_error = initial_error
|
105
|
+
|
106
|
+
# Check if the parsed_data exists but just failed validation
|
107
|
+
# This might mean we have the right structure but wrong values
|
108
|
+
if parsed_data is not None and isinstance(parsed_data, dict):
|
109
|
+
logging.debug(
|
110
|
+
f"Have parsed_data dict, checking if it's close to valid..."
|
111
|
+
)
|
112
|
+
# If we got a dict with the right keys, maybe we just need to clean it up
|
113
|
+
expected_fields = set(request_type.model_fields.keys())
|
114
|
+
parsed_fields = set(parsed_data.keys())
|
115
|
+
if expected_fields == parsed_fields and all(
|
116
|
+
parsed_data.get(k) is not None for k in expected_fields
|
117
|
+
):
|
118
|
+
# We have the right structure with non-None values, don't retry with parse model
|
119
|
+
logging.debug(
|
120
|
+
"Structure matches with valid values, returning original error"
|
121
|
+
)
|
122
|
+
if handle_validation == "raise":
|
123
|
+
raise ValueError(
|
124
|
+
f"Failed to parse response: {initial_error}"
|
125
|
+
) from initial_error
|
126
|
+
elif handle_validation == "return_none":
|
127
|
+
return None
|
128
|
+
else:
|
129
|
+
return text
|
130
|
+
|
131
|
+
while num_try < max_retries:
|
132
|
+
num_try += 1
|
133
|
+
|
134
|
+
try:
|
135
|
+
logging.debug(f"Retry {num_try}: Using parse model to reformat")
|
136
|
+
_, res = await branch.chat(
|
137
|
+
instruction="reformat text into specified model",
|
138
|
+
guidance="follow the required response format, using the model schema as a guide",
|
139
|
+
context=[{"text_to_format": text}],
|
140
|
+
response_format=request_type,
|
141
|
+
sender=branch.user,
|
142
|
+
recipient=branch.id,
|
143
|
+
imodel=branch.parse_model,
|
144
|
+
return_ins_res_message=True,
|
145
|
+
)
|
146
|
+
|
147
|
+
# Try to parse the reformatted response
|
148
|
+
parsed_data = fuzzy_validate_mapping(
|
67
149
|
res.response,
|
68
150
|
breakdown_pydantic_annotation(request_type),
|
69
151
|
similarity_algo=similarity_algo,
|
@@ -75,25 +157,31 @@ async def parse(
|
|
75
157
|
strict=strict,
|
76
158
|
suppress_conversion_errors=suppress_conversion_errors,
|
77
159
|
)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
break
|
87
|
-
|
88
|
-
if not isinstance(response_model, BaseModel):
|
89
|
-
match handle_validation:
|
90
|
-
case "return_value":
|
160
|
+
|
161
|
+
if operative is not None:
|
162
|
+
response_model = operative.update_response_model(parsed_data)
|
163
|
+
else:
|
164
|
+
response_model = request_type.model_validate(parsed_data)
|
165
|
+
|
166
|
+
# If successful, return
|
167
|
+
if isinstance(response_model, BaseModel):
|
91
168
|
return response_model
|
92
|
-
case "return_none":
|
93
|
-
return None
|
94
|
-
case "raise":
|
95
|
-
raise ValueError(
|
96
|
-
"Failed to parse response into request format"
|
97
|
-
)
|
98
169
|
|
99
|
-
|
170
|
+
except InterruptedError as e:
|
171
|
+
raise e
|
172
|
+
except Exception as e:
|
173
|
+
last_error = e
|
174
|
+
# Continue to next retry
|
175
|
+
continue
|
176
|
+
|
177
|
+
# All retries exhausted
|
178
|
+
match handle_validation:
|
179
|
+
case "return_value":
|
180
|
+
return text
|
181
|
+
case "return_none":
|
182
|
+
return None
|
183
|
+
case "raise":
|
184
|
+
error_msg = "Failed to parse response into request format"
|
185
|
+
if last_error:
|
186
|
+
error_msg += f": {last_error}"
|
187
|
+
raise ValueError(error_msg) from last_error
|
@@ -19,7 +19,7 @@ from pathlib import Path
|
|
19
19
|
from typing import Any, ClassVar, Generic, TypeVar
|
20
20
|
|
21
21
|
import pandas as pd
|
22
|
-
from pydantic import Field
|
22
|
+
from pydantic import Field
|
23
23
|
from pydantic.fields import FieldInfo
|
24
24
|
from typing_extensions import Self, override
|
25
25
|
|
@@ -910,9 +910,23 @@ class Pile(Element, Collective[E], Generic[E]):
|
|
910
910
|
self.progression.insert(index, item_order)
|
911
911
|
self.collections.update(item_dict)
|
912
912
|
|
913
|
-
|
914
|
-
|
915
|
-
|
913
|
+
def to_dict(self) -> dict[str, Any]:
|
914
|
+
"""Convert pile to dictionary, properly handling collections."""
|
915
|
+
# Get base dict from parent class
|
916
|
+
dict_ = super().to_dict()
|
917
|
+
|
918
|
+
# Manually serialize collections
|
919
|
+
collections_list = []
|
920
|
+
for item in self.collections.values():
|
921
|
+
if hasattr(item, "to_dict"):
|
922
|
+
collections_list.append(item.to_dict())
|
923
|
+
elif hasattr(item, "model_dump"):
|
924
|
+
collections_list.append(item.model_dump())
|
925
|
+
else:
|
926
|
+
collections_list.append(str(item))
|
927
|
+
|
928
|
+
dict_["collections"] = collections_list
|
929
|
+
return dict_
|
916
930
|
|
917
931
|
class AsyncPileIterator:
|
918
932
|
def __init__(self, pile: Pile):
|