lionagi 0.15.9__py3-none-any.whl → 0.15.13__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/__init__.py CHANGED
@@ -7,9 +7,9 @@ import logging
7
7
  from pydantic import BaseModel, Field
8
8
 
9
9
  from . import _types as types
10
- from .operations import Operation
11
- from .operations import OperationGraphBuilder as Builder
12
- from .operations import brainstorm, flow, plan
10
+ from . import ln as ln
11
+ from .operations.builder import OperationGraphBuilder as Builder
12
+ from .operations.node import Operation
13
13
  from .service.imodel import iModel
14
14
  from .session.session import Branch, Session
15
15
  from .version import __version__
@@ -28,7 +28,5 @@ __all__ = (
28
28
  "logger",
29
29
  "Builder",
30
30
  "Operation",
31
- "brainstorm",
32
- "flow",
33
- "plan",
31
+ "ln",
34
32
  )
@@ -1,16 +1,21 @@
1
1
  """
2
- Clean LionAGI async PostgreSQL adapter for integration into lionagi core.
3
-
4
- This adapter handles SQLAlchemy async inspection issues and lionagi data
5
- serialization while providing seamless async persistence.
2
+ Simplified LionAGI async PostgreSQL adapter for pydapter v1.0.4+
3
+
4
+ This adapter leverages pydapter's improved raw SQL handling.
5
+ No workarounds needed - pydapter now properly handles:
6
+ - Raw SQL without table parameter
7
+ - No table inspection for raw SQL
8
+ - ORDER BY operations
9
+ - Both SQLite and PostgreSQL connections
6
10
  """
7
11
 
8
12
  from __future__ import annotations
9
13
 
10
- from datetime import datetime
11
- from typing import Any, ClassVar, TypeVar
14
+ from typing import ClassVar, TypeVar
12
15
 
13
- from pydapter.exceptions import QueryError
16
+ import sqlalchemy as sa
17
+ from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
18
+ from sqlalchemy.ext.asyncio import create_async_engine
14
19
 
15
20
  from ._utils import check_async_postgres_available
16
21
 
@@ -19,61 +24,21 @@ _ASYNC_POSTGRES_AVAILABLE = check_async_postgres_available()
19
24
  if isinstance(_ASYNC_POSTGRES_AVAILABLE, ImportError):
20
25
  raise _ASYNC_POSTGRES_AVAILABLE
21
26
 
22
- import sqlalchemy as sa
23
- from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
24
- from sqlalchemy.ext.asyncio import create_async_engine
25
-
26
27
  T = TypeVar("T")
27
28
 
28
29
 
29
30
  class LionAGIAsyncPostgresAdapter(AsyncPostgresAdapter[T]):
30
31
  """
31
- Async PostgreSQL adapter for lionagi Nodes with critical fixes.
32
-
33
- Solves core issues:
34
- 1. SQLAlchemy async table inspection ("Inspection on an AsyncConnection is currently not supported")
35
- 2. LionAGI float timestamp serialization (created_at as float → datetime)
36
- 3. Datetime objects in JSON content (datetime → ISO strings)
37
- 4. Automatic metadata field mapping via LionAGIPostgresAdapter
32
+ Streamlined async adapter for lionagi Nodes.
38
33
 
39
34
  Features:
40
- - Works with lionagi's adapt_to_async() system
41
- - Automatic schema creation for lionagi Node structure
42
- - Cross-database compatibility (PostgreSQL/SQLite)
43
- - Handles all lionagi data serialization edge cases
35
+ - Auto-creates tables with lionagi schema
36
+ - Inherits all pydapter v1.0.4+ improvements
37
+ - No workarounds needed for SQLite or raw SQL
44
38
  """
45
39
 
46
40
  obj_key: ClassVar[str] = "lionagi_async_pg"
47
41
 
48
- @classmethod
49
- def _table(cls, meta: sa.MetaData, name: str) -> sa.Table:
50
- """
51
- Override parent's _table to avoid async inspection issues.
52
-
53
- Uses JSON for SQLite compatibility, JSONB for PostgreSQL performance.
54
- """
55
- # Determine JSON type based on database (check connection URL if available)
56
- json_type = sa.JSON # Default safe option that works everywhere
57
-
58
- # Try to detect PostgreSQL from the connection
59
- if hasattr(meta, "bind") and meta.bind:
60
- engine_url = str(meta.bind.engine.url)
61
- if "postgresql" in engine_url and "sqlite" not in engine_url:
62
- json_type = sa.dialects.postgresql.JSONB
63
-
64
- return sa.Table(
65
- name,
66
- meta,
67
- sa.Column("id", sa.String, primary_key=True),
68
- sa.Column("content", json_type),
69
- sa.Column(
70
- "node_metadata", json_type
71
- ), # mapped from lionagi metadata
72
- sa.Column("created_at", sa.DateTime),
73
- sa.Column("embedding", json_type),
74
- # Note: No autoload_with to avoid async inspection error
75
- )
76
-
77
42
  @classmethod
78
43
  async def to_obj(
79
44
  cls,
@@ -81,282 +46,53 @@ class LionAGIAsyncPostgresAdapter(AsyncPostgresAdapter[T]):
81
46
  /,
82
47
  *,
83
48
  many: bool = True,
84
- adapt_meth: str = "model_dump",
49
+ adapt_meth: str = None,
85
50
  **kw,
86
51
  ):
87
- """
88
- Write lionagi Node(s) to PostgreSQL with automatic fixes.
89
-
90
- Handles:
91
- 1. Table creation if needed
92
- 2. LionAGI data serialization fixes
93
- 3. Async database operations
94
- """
95
- try:
96
- # Validate required parameters
97
- engine_url = kw.get("dsn") or kw.get("engine_url")
98
- table = kw.get("table")
99
-
100
- if not engine_url or not table:
101
- raise ValueError(
102
- "Missing required 'dsn' and 'table' parameters"
103
- )
104
-
105
- # Ensure table exists with lionagi schema
106
- await cls._ensure_table_exists(engine_url, table)
107
-
108
- # Prepare data with lionagi fixes
109
- items = subj if isinstance(subj, list) else [subj]
110
- if not items:
111
- return {"inserted_count": 0}
112
-
113
- # Convert nodes to database rows with serialization fixes
114
- rows = []
115
- for item in items:
116
- data = getattr(item, adapt_meth)()
117
- fixed_data = cls._fix_lionagi_data(data)
118
- rows.append(fixed_data)
119
-
120
- # Execute async insert
121
- engine = create_async_engine(engine_url, future=True)
122
- async with engine.begin() as conn:
123
- meta = sa.MetaData()
124
- meta.bind = conn
125
- table_obj = cls._table(meta, table)
126
- await conn.execute(sa.insert(table_obj), rows)
127
-
128
- return {"inserted_count": len(rows)}
129
-
130
- except Exception as e:
131
- raise QueryError(
132
- f"Error in lionagi async adapter: {e}",
133
- adapter="lionagi_async_pg",
134
- ) from e
52
+ """Write lionagi Node(s) to database with auto-table creation."""
53
+ # Auto-create table if needed
54
+ if table := kw.get("table"):
55
+ if engine_url := (kw.get("dsn") or kw.get("engine_url")):
56
+ await cls._ensure_table(engine_url, table)
57
+ elif engine := kw.get("engine"):
58
+ await cls._ensure_table(engine, table)
59
+
60
+ return await super().to_obj(
61
+ subj, many=many, adapt_meth=adapt_meth, **kw
62
+ )
135
63
 
136
64
  @classmethod
137
- async def _ensure_table_exists(cls, engine_url: str, table_name: str):
65
+ async def _ensure_table(cls, engine_or_url, table_name: str):
138
66
  """Create table with lionagi schema if it doesn't exist."""
139
- try:
140
- engine = create_async_engine(engine_url, future=True)
141
- async with engine.begin() as conn:
142
- meta = sa.MetaData()
143
- meta.bind = conn
144
-
145
- # Use the same _table method to ensure consistency
146
- table = cls._table(meta, table_name)
147
-
148
- # Create just this table
149
- await conn.run_sync(table.create, checkfirst=True)
150
-
151
- except Exception:
152
- # Table might already exist, continue
153
- pass
154
-
155
- @classmethod
156
- def _fix_lionagi_data(cls, data: dict) -> dict:
157
- """
158
- Fix lionagi Node data for database storage.
159
-
160
- Handles:
161
- 1. Float timestamp → datetime for created_at
162
- 2. Datetime objects in content → ISO strings
163
- """
164
- # Fix created_at timestamp
165
- if "created_at" in data and isinstance(
166
- data["created_at"], (int, float)
167
- ):
168
- data["created_at"] = datetime.fromtimestamp(data["created_at"])
169
-
170
- # Fix datetime objects in content
171
- if "content" in data and isinstance(data["content"], dict):
172
- data["content"] = cls._serialize_datetime_recursive(
173
- data["content"]
174
- )
175
-
176
- return data
177
-
178
- @classmethod
179
- def _serialize_datetime_recursive(cls, obj: Any) -> Any:
180
- """Recursively convert datetime objects to ISO strings."""
181
- if isinstance(obj, datetime):
182
- return obj.isoformat()
183
- elif isinstance(obj, dict):
184
- return {
185
- k: cls._serialize_datetime_recursive(v) for k, v in obj.items()
186
- }
187
- elif isinstance(obj, list):
188
- return [cls._serialize_datetime_recursive(item) for item in obj]
67
+ should_dispose = False
68
+ if isinstance(engine_or_url, str):
69
+ engine = create_async_engine(engine_or_url, future=True)
70
+ should_dispose = True
189
71
  else:
190
- return obj
191
-
192
- @classmethod
193
- async def from_obj(
194
- cls,
195
- node_cls: type[T],
196
- obj: Any,
197
- /,
198
- *,
199
- adapt_meth: str = "from_dict",
200
- many: bool = True,
201
- **kw,
202
- ) -> T | list[T] | None:
203
- """
204
- Read lionagi Node(s) from database with automatic data reconstruction.
205
-
206
- Handles:
207
- 1. Database querying with filters
208
- 2. Reverse metadata field mapping (node_metadata → metadata)
209
- 3. Reverse data serialization (ISO strings → datetime objects)
210
- 4. Node object reconstruction
72
+ engine = engine_or_url
211
73
 
212
- Args:
213
- node_cls: The Node class to instantiate
214
- obj: Database connection parameters (dict with dsn, table, etc.)
215
- adapt_meth: Adaptation method (unused but required by pydapter)
216
- many: Whether to return list or single object
217
- **kw: Additional query parameters (where, limit, order_by)
218
-
219
- Returns:
220
- Single Node, list of Nodes, or None if no results found
221
- """
222
74
  try:
223
- # Merge obj parameters with kw parameters
224
- if isinstance(obj, dict):
225
- params = {**obj, **kw}
226
- else:
227
- params = kw
228
-
229
- # Validate required parameters
230
- engine_url = params.get("dsn") or params.get("engine_url")
231
- table = params.get("table")
232
-
233
- if not engine_url or not table:
234
- raise ValueError(
235
- "Missing required 'dsn' and 'table' parameters"
236
- )
237
-
238
- # Build query
239
- engine = create_async_engine(engine_url, future=True)
240
75
  async with engine.begin() as conn:
241
- meta = sa.MetaData()
242
- meta.bind = conn
243
- table_obj = cls._table(meta, table)
244
-
245
- # Build SELECT query
246
- query = sa.select(table_obj)
247
-
248
- # Add WHERE conditions if provided
249
- where_conditions = params.get("where")
250
- if where_conditions:
251
- if isinstance(where_conditions, dict):
252
- # Convert dict to column conditions
253
- for col_name, value in where_conditions.items():
254
- if hasattr(table_obj.c, col_name):
255
- query = query.where(
256
- getattr(table_obj.c, col_name) == value
257
- )
258
- else:
259
- # Assume it's already a SQLAlchemy condition
260
- query = query.where(where_conditions)
261
-
262
- # Add ordering if provided
263
- order_by = params.get("order_by")
264
- if order_by:
265
- if isinstance(order_by, str):
266
- if hasattr(table_obj.c, order_by):
267
- query = query.order_by(
268
- getattr(table_obj.c, order_by)
269
- )
270
- else:
271
- query = query.order_by(order_by)
272
-
273
- # Add limit if provided
274
- limit = params.get("limit")
275
- if limit:
276
- query = query.limit(limit)
277
-
278
- # Execute query
279
- result = await conn.execute(query)
280
- rows = result.fetchall()
281
-
282
- # Use many parameter from params if provided, otherwise use method parameter
283
- return_many = params.get("many", many)
284
-
285
- if not rows:
286
- return [] if return_many else None
287
-
288
- # Convert database rows back to Node objects
289
- nodes = []
290
- for row in rows:
291
- # Convert row to dict
292
- row_dict = dict(row._mapping)
293
-
294
- # Apply reverse lionagi data transformations
295
- node_data = cls._reverse_lionagi_data(row_dict)
296
-
297
- # Create Node instance
298
- node = node_cls(**node_data)
299
- nodes.append(node)
300
-
301
- if return_many:
302
- return nodes
303
- else:
304
- return nodes[-1] if nodes else None
305
-
306
- except Exception as e:
307
- raise QueryError(
308
- f"Error reading from lionagi async adapter: {e}",
309
- adapter="lionagi_async_pg",
310
- ) from e
311
-
312
- @classmethod
313
- def _reverse_lionagi_data(cls, row_data: dict) -> dict:
314
- """
315
- Reverse lionagi data transformations from database storage.
316
-
317
- Handles:
318
- 1. Database field mapping (node_metadata → metadata)
319
- 2. ISO string → datetime objects in content
320
- 3. Proper lionagi Node field structure
321
- """
322
- # Create a copy to avoid modifying original
323
- data = row_data.copy()
324
-
325
- # Reverse field mapping: node_metadata → metadata
326
- if "node_metadata" in data:
327
- data["metadata"] = data.pop("node_metadata")
328
-
329
- # Reverse datetime serialization in content
330
- if "content" in data and isinstance(data["content"], dict):
331
- data["content"] = cls._deserialize_datetime_recursive(
332
- data["content"]
333
- )
334
-
335
- return data
76
+ # Determine JSON type based on database
77
+ engine_url = str(engine.url)
78
+ json_type = (
79
+ sa.dialects.postgresql.JSONB
80
+ if "postgresql" in engine_url
81
+ else sa.JSON
82
+ )
336
83
 
337
- @classmethod
338
- def _deserialize_datetime_recursive(cls, obj: Any) -> Any:
339
- """Recursively convert ISO datetime strings back to datetime objects."""
340
- if isinstance(obj, str):
341
- # Try to parse as ISO datetime string
342
- try:
343
- # Check if it looks like an ISO datetime string
344
- if "T" in obj and (
345
- obj.endswith("Z")
346
- or "+" in obj[-10:]
347
- or obj.count(":") >= 2
348
- ):
349
- return datetime.fromisoformat(obj.replace("Z", "+00:00"))
350
- except (ValueError, AttributeError):
351
- # Not a datetime string, return as-is
352
- pass
353
- return obj
354
- elif isinstance(obj, dict):
355
- return {
356
- k: cls._deserialize_datetime_recursive(v)
357
- for k, v in obj.items()
358
- }
359
- elif isinstance(obj, list):
360
- return [cls._deserialize_datetime_recursive(item) for item in obj]
361
- else:
362
- return obj
84
+ # Create table with lionagi schema
85
+ await conn.run_sync(
86
+ lambda sync_conn: sa.Table(
87
+ table_name,
88
+ sa.MetaData(),
89
+ sa.Column("id", sa.String, primary_key=True),
90
+ sa.Column("content", json_type),
91
+ sa.Column("node_metadata", json_type),
92
+ sa.Column("created_at", sa.DateTime),
93
+ sa.Column("embedding", json_type, nullable=True),
94
+ ).create(sync_conn, checkfirst=True)
95
+ )
96
+ finally:
97
+ if should_dispose:
98
+ await engine.dispose()
@@ -0,0 +1,10 @@
1
+ def check_docling_available():
2
+ try:
3
+ from docling.document_converter import DocumentConverter # noqa: F401
4
+
5
+ return True
6
+ except Exception:
7
+ return ImportError(
8
+ "The 'docling' package is required for this feature. "
9
+ "Please install it via 'pip install lionagi[reader]'."
10
+ )
@@ -8,11 +8,14 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
8
8
  from pathlib import Path
9
9
  from typing import Any, Literal
10
10
 
11
- from lionagi.utils import lcall
11
+ from lionagi import ln
12
12
 
13
+ from ._utils import check_docling_available
13
14
  from .chunk import chunk_content
14
15
  from .save import save_chunks
15
16
 
17
+ _HAS_DOCLING = check_docling_available()
18
+
16
19
 
17
20
  def dir_to_files(
18
21
  directory: str | Path,
@@ -206,24 +209,24 @@ def chunk(
206
209
  reader_tool = lambda x: Path(x).read_text(encoding="utf-8")
207
210
 
208
211
  if reader_tool == "docling":
209
- from lionagi.libs.package.imports import check_import
212
+ if _HAS_DOCLING is not True:
213
+ raise _HAS_DOCLING
210
214
 
211
- DocumentConverter = check_import(
212
- "docling",
213
- module_name="document_converter",
214
- import_name="DocumentConverter",
215
+ from docling.document_converter import ( # noqa: F401
216
+ DocumentConverter,
215
217
  )
218
+
216
219
  converter = DocumentConverter()
217
220
  reader_tool = lambda x: converter.convert(
218
221
  x
219
222
  ).document.export_to_markdown()
220
223
 
221
- texts = lcall(files, reader_tool)
224
+ texts = ln.lcall(files, reader_tool)
222
225
 
223
226
  else:
224
227
  texts = [text]
225
228
 
226
- chunks = lcall(
229
+ chunks = ln.lcall(
227
230
  texts,
228
231
  chunk_content,
229
232
  chunk_by=chunk_by,
@@ -244,15 +247,15 @@ def chunk(
244
247
  output_file = Path(output_file)
245
248
  if output_file.suffix == ".csv":
246
249
  p = Pile(chunks)
247
- p.to_csv_file(output_file)
250
+ p.dump(output_file, "csv")
248
251
 
249
- elif output_file.suffix == ".json":
252
+ if output_file.suffix == "json":
250
253
  p = Pile(chunks)
251
- p.to_json_file(output_file, use_pd=True)
254
+ p.dump(output_file, "json")
252
255
 
253
- elif output_file.suffix in Pile.list_adapters():
256
+ if output_file.suffix == ".parquet":
254
257
  p = Pile(chunks)
255
- p.adapt_to(output_file.suffix, fp=output_file)
258
+ p.dump(output_file, "parquet")
256
259
 
257
260
  else:
258
261
  raise ValueError(f"Unsupported output file format: {output_file}")
@@ -1,4 +1,4 @@
1
- from lionagi.utils import check_import, is_import_installed
1
+ from lionagi.utils import import_module, is_import_installed
2
2
 
3
3
  _HAS_PDF2IMAGE = is_import_installed("pdf2image")
4
4
 
@@ -25,7 +25,7 @@ def pdf_to_images(
25
25
 
26
26
  import os
27
27
 
28
- convert_from_path = check_import(
28
+ convert_from_path = import_module(
29
29
  "pdf2image", import_name="convert_from_path"
30
30
  )
31
31
 
@@ -320,13 +320,13 @@ def string_similarity(
320
320
  # Sort by score (descending) and index (ascending) for stable ordering
321
321
  results.sort(key=lambda x: (-x.score, x.index))
322
322
 
323
- # Return results
324
- if return_most_similar:
325
- return results[0].word
326
-
327
323
  # Filter exact matches for case sensitive comparisons
328
324
  if case_sensitive:
329
325
  max_score = results[0].score
330
326
  results = [r for r in results if r.score == max_score]
331
327
 
328
+ # Return results
329
+ if return_most_similar:
330
+ return results[0].word
331
+
332
332
  return [r.word for r in results]
lionagi/ln/__init__.py CHANGED
@@ -1,4 +1,6 @@
1
1
  from ._async_call import AlcallParams, BcallParams, alcall, bcall
2
+ from ._extract_json import extract_json
3
+ from ._fuzzy_json import fuzzy_json
2
4
  from ._hash import hash_dict
3
5
  from ._json_dump import (
4
6
  DEFAULT_SERIALIZER,
@@ -56,4 +58,30 @@ __all__ = (
56
58
  "DEFAULT_SERIALIZER",
57
59
  "DEFAULT_SERIALIZER_OPTION",
58
60
  "json_dumps",
61
+ "TaskGroup",
62
+ "create_task_group",
63
+ "CancelScope",
64
+ "move_on_after",
65
+ "fail_after",
66
+ "ConnectionPool",
67
+ "WorkerPool",
68
+ "parallel_requests",
69
+ "retry_with_timeout",
70
+ "Lock",
71
+ "Semaphore",
72
+ "CapacityLimiter",
73
+ "Event",
74
+ "Condition",
75
+ "get_cancelled_exc_class",
76
+ "shield",
77
+ "ResourceTracker",
78
+ "resource_leak_detector",
79
+ "track_resource",
80
+ "untrack_resource",
81
+ "cleanup_check",
82
+ "get_global_tracker",
83
+ "is_coro_func",
84
+ "ConcurrencyEvent",
85
+ "fuzzy_json",
86
+ "extract_json",
59
87
  )
lionagi/ln/_async_call.py CHANGED
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Any, ClassVar
5
5
 
6
6
  import anyio
7
+ import anyio.to_thread
7
8
  from pydantic import BaseModel
8
9
 
9
10
  from ._models import Params
@@ -0,0 +1,60 @@
1
+ import re
2
+ from typing import Any
3
+
4
+ import orjson
5
+
6
+ from ._fuzzy_json import fuzzy_json
7
+
8
+ # Precompile the regex for extracting JSON code blocks
9
+ _JSON_BLOCK_PATTERN = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
10
+
11
+
12
+ def extract_json(
13
+ input_data: str | list[str],
14
+ /,
15
+ *,
16
+ fuzzy_parse: bool = False,
17
+ return_one_if_single: bool = True,
18
+ ) -> dict[str, Any] | list[dict[str, Any]]:
19
+ """Extract and parse JSON content from a string or markdown code blocks.
20
+ Attempts direct JSON parsing first. If that fails, looks for JSON content
21
+ within markdown code blocks denoted by ```json.
22
+
23
+ Args:
24
+ input_data (str | list[str]): The input string or list of strings to parse.
25
+ fuzzy_parse (bool): If True, attempts fuzzy JSON parsing on failed attempts.
26
+ return_one_if_single (bool): If True and only one JSON object is found,
27
+ returns a dict instead of a list with one dict.
28
+ """
29
+
30
+ # If input_data is a list, join into a single string
31
+ if isinstance(input_data, list):
32
+ input_str = "\n".join(input_data)
33
+ else:
34
+ input_str = input_data
35
+
36
+ # 1. Try direct parsing
37
+ try:
38
+ if fuzzy_parse:
39
+ return fuzzy_json(input_str)
40
+ return orjson.loads(input_str)
41
+ except Exception:
42
+ pass
43
+
44
+ # 2. Attempt extracting JSON blocks from markdown
45
+ matches = _JSON_BLOCK_PATTERN.findall(input_str)
46
+ if not matches:
47
+ return []
48
+
49
+ # If only one match, return single dict; if multiple, return list of dicts
50
+ if return_one_if_single and len(matches) == 1:
51
+ data_str = matches[0]
52
+ if fuzzy_parse:
53
+ return fuzzy_json(data_str)
54
+ return orjson.loads(data_str)
55
+
56
+ # Multiple matches
57
+ if fuzzy_parse:
58
+ return [fuzzy_json(m) for m in matches]
59
+ else:
60
+ return [orjson.loads(m) for m in matches]