lionagi 0.15.9__py3-none-any.whl → 0.15.11__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,16 @@
1
1
  """
2
2
  Clean LionAGI async PostgreSQL adapter for integration into lionagi core.
3
3
 
4
- This adapter handles SQLAlchemy async inspection issues and lionagi data
5
- serialization while providing seamless async persistence.
4
+ This adapter leverages pydapter v1.0.2+ CRUD operations.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
9
8
 
10
- from datetime import datetime
11
- from typing import Any, ClassVar, TypeVar
9
+ from typing import ClassVar, TypeVar
12
10
 
13
- from pydapter.exceptions import QueryError
11
+ import sqlalchemy as sa
12
+ from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
13
+ from sqlalchemy.ext.asyncio import create_async_engine
14
14
 
15
15
  from ._utils import check_async_postgres_available
16
16
 
@@ -19,61 +19,20 @@ _ASYNC_POSTGRES_AVAILABLE = check_async_postgres_available()
19
19
  if isinstance(_ASYNC_POSTGRES_AVAILABLE, ImportError):
20
20
  raise _ASYNC_POSTGRES_AVAILABLE
21
21
 
22
- import sqlalchemy as sa
23
- from pydapter.extras.async_postgres_ import AsyncPostgresAdapter
24
- from sqlalchemy.ext.asyncio import create_async_engine
25
-
26
22
  T = TypeVar("T")
27
23
 
28
24
 
29
25
  class LionAGIAsyncPostgresAdapter(AsyncPostgresAdapter[T]):
30
26
  """
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
27
+ Zero-config async PostgreSQL adapter for lionagi Nodes.
38
28
 
39
- 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
29
+ - Auto-creates tables with lionagi schema
30
+ - Changes default adapt_meth to "to_dict" for lionagi Elements
31
+ - Everything else handled by parent AsyncPostgresAdapter
44
32
  """
45
33
 
46
34
  obj_key: ClassVar[str] = "lionagi_async_pg"
47
35
 
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
36
  @classmethod
78
37
  async def to_obj(
79
38
  cls,
@@ -81,282 +40,59 @@ class LionAGIAsyncPostgresAdapter(AsyncPostgresAdapter[T]):
81
40
  /,
82
41
  *,
83
42
  many: bool = True,
84
- adapt_meth: str = "model_dump",
43
+ adapt_meth: str = "as_jsonable", # Default to to_dict for lionagi
85
44
  **kw,
86
45
  ):
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
46
+ """Write lionagi Node(s) to PostgreSQL with CRUD support."""
47
+ # Auto-create table if needed
48
+ if table := kw.get("table"):
49
+ if engine_url := (kw.get("dsn") or kw.get("engine_url")):
50
+ await cls._ensure_table(engine_url, table)
51
+ elif engine := kw.get("engine"):
52
+ await cls._ensure_table(engine, table)
53
+
54
+ return await super().to_obj(
55
+ subj, many=many, adapt_meth=adapt_meth, **kw
56
+ )
135
57
 
136
58
  @classmethod
137
- async def _ensure_table_exists(cls, engine_url: str, table_name: str):
59
+ async def _ensure_table(cls, engine_or_url, table_name: str):
138
60
  """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]
61
+ # Handle both engine and URL
62
+ should_dispose = None
63
+ if isinstance(engine_or_url, str):
64
+ engine = create_async_engine(engine_or_url, future=True)
65
+ should_dispose = True
189
66
  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
67
+ engine = engine_or_url
68
+ should_dispose = False
211
69
 
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
70
  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
71
  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
72
+ # Determine JSON type based on database
73
+ engine_url = str(engine.url)
74
+ json_type = (
75
+ sa.dialects.postgresql.JSONB
76
+ if "postgresql" in engine_url
77
+ else sa.JSON
78
+ )
336
79
 
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
80
+ # Create table with lionagi schema
81
+ await conn.run_sync(
82
+ lambda sync_conn: sa.Table(
83
+ table_name,
84
+ sa.MetaData(),
85
+ sa.Column("id", sa.String, primary_key=True),
86
+ sa.Column("content", json_type),
87
+ sa.Column(
88
+ "metadata", json_type
89
+ ), # Use metadata directly now
90
+ sa.Column(
91
+ "created_at", sa.Float
92
+ ), # Stored as float timestamp
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
  )
@@ -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]