lionherd-core 1.0.0a3__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.
- lionherd_core/__init__.py +84 -0
- lionherd_core/base/__init__.py +30 -0
- lionherd_core/base/_utils.py +295 -0
- lionherd_core/base/broadcaster.py +128 -0
- lionherd_core/base/element.py +300 -0
- lionherd_core/base/event.py +322 -0
- lionherd_core/base/eventbus.py +112 -0
- lionherd_core/base/flow.py +236 -0
- lionherd_core/base/graph.py +616 -0
- lionherd_core/base/node.py +212 -0
- lionherd_core/base/pile.py +811 -0
- lionherd_core/base/progression.py +261 -0
- lionherd_core/errors.py +104 -0
- lionherd_core/libs/__init__.py +2 -0
- lionherd_core/libs/concurrency/__init__.py +60 -0
- lionherd_core/libs/concurrency/_cancel.py +85 -0
- lionherd_core/libs/concurrency/_errors.py +80 -0
- lionherd_core/libs/concurrency/_patterns.py +238 -0
- lionherd_core/libs/concurrency/_primitives.py +253 -0
- lionherd_core/libs/concurrency/_priority_queue.py +135 -0
- lionherd_core/libs/concurrency/_resource_tracker.py +66 -0
- lionherd_core/libs/concurrency/_task.py +58 -0
- lionherd_core/libs/concurrency/_utils.py +61 -0
- lionherd_core/libs/schema_handlers/__init__.py +35 -0
- lionherd_core/libs/schema_handlers/_function_call_parser.py +122 -0
- lionherd_core/libs/schema_handlers/_minimal_yaml.py +88 -0
- lionherd_core/libs/schema_handlers/_schema_to_model.py +251 -0
- lionherd_core/libs/schema_handlers/_typescript.py +153 -0
- lionherd_core/libs/string_handlers/__init__.py +15 -0
- lionherd_core/libs/string_handlers/_extract_json.py +65 -0
- lionherd_core/libs/string_handlers/_fuzzy_json.py +103 -0
- lionherd_core/libs/string_handlers/_string_similarity.py +347 -0
- lionherd_core/libs/string_handlers/_to_num.py +63 -0
- lionherd_core/ln/__init__.py +45 -0
- lionherd_core/ln/_async_call.py +314 -0
- lionherd_core/ln/_fuzzy_match.py +166 -0
- lionherd_core/ln/_fuzzy_validate.py +151 -0
- lionherd_core/ln/_hash.py +141 -0
- lionherd_core/ln/_json_dump.py +347 -0
- lionherd_core/ln/_list_call.py +110 -0
- lionherd_core/ln/_to_dict.py +373 -0
- lionherd_core/ln/_to_list.py +190 -0
- lionherd_core/ln/_utils.py +156 -0
- lionherd_core/lndl/__init__.py +62 -0
- lionherd_core/lndl/errors.py +30 -0
- lionherd_core/lndl/fuzzy.py +321 -0
- lionherd_core/lndl/parser.py +427 -0
- lionherd_core/lndl/prompt.py +137 -0
- lionherd_core/lndl/resolver.py +323 -0
- lionherd_core/lndl/types.py +287 -0
- lionherd_core/protocols.py +181 -0
- lionherd_core/py.typed +0 -0
- lionherd_core/types/__init__.py +46 -0
- lionherd_core/types/_sentinel.py +131 -0
- lionherd_core/types/base.py +341 -0
- lionherd_core/types/operable.py +133 -0
- lionherd_core/types/spec.py +313 -0
- lionherd_core/types/spec_adapters/__init__.py +10 -0
- lionherd_core/types/spec_adapters/_protocol.py +125 -0
- lionherd_core/types/spec_adapters/pydantic_field.py +177 -0
- lionherd_core-1.0.0a3.dist-info/METADATA +502 -0
- lionherd_core-1.0.0a3.dist-info/RECORD +64 -0
- lionherd_core-1.0.0a3.dist-info/WHEEL +4 -0
- lionherd_core-1.0.0a3.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Copyright (c) 2025, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from .._sentinel import Unset, is_sentinel
|
|
8
|
+
from ._protocol import SpecAdapter
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from pydantic.fields import FieldInfo
|
|
13
|
+
|
|
14
|
+
from ..operable import Operable
|
|
15
|
+
from ..spec import Spec
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@functools.lru_cache(maxsize=1)
|
|
19
|
+
def _get_pydantic_field_params() -> set[str]:
|
|
20
|
+
"""Get valid Pydantic Field params (cached)."""
|
|
21
|
+
import inspect
|
|
22
|
+
|
|
23
|
+
from pydantic import Field as PydanticField
|
|
24
|
+
|
|
25
|
+
params = set(inspect.signature(PydanticField).parameters.keys())
|
|
26
|
+
params.discard("kwargs")
|
|
27
|
+
return params
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PydanticSpecAdapter(SpecAdapter["BaseModel"]):
|
|
31
|
+
"""Pydantic adapter: Spec → FieldInfo → BaseModel."""
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def create_field(cls, spec: "Spec") -> "FieldInfo":
|
|
35
|
+
"""Create Pydantic FieldInfo from Spec."""
|
|
36
|
+
from pydantic import Field as PydanticField
|
|
37
|
+
|
|
38
|
+
# Get valid Pydantic Field parameters (cached)
|
|
39
|
+
pydantic_field_params = _get_pydantic_field_params()
|
|
40
|
+
|
|
41
|
+
# Extract metadata for FieldInfo
|
|
42
|
+
field_kwargs = {}
|
|
43
|
+
|
|
44
|
+
if not is_sentinel(spec.metadata, none_as_sentinel=True):
|
|
45
|
+
for meta in spec.metadata:
|
|
46
|
+
if meta.key == "default":
|
|
47
|
+
# Handle callable defaults as default_factory
|
|
48
|
+
if callable(meta.value):
|
|
49
|
+
field_kwargs["default_factory"] = meta.value
|
|
50
|
+
else:
|
|
51
|
+
field_kwargs["default"] = meta.value
|
|
52
|
+
elif meta.key == "validator":
|
|
53
|
+
# Validators are handled separately in create_model
|
|
54
|
+
continue
|
|
55
|
+
elif meta.key in pydantic_field_params:
|
|
56
|
+
# Pass through standard Pydantic field attributes
|
|
57
|
+
field_kwargs[meta.key] = meta.value
|
|
58
|
+
elif meta.key in {"nullable", "listable"}:
|
|
59
|
+
# These are FieldTemplate markers, don't pass to FieldInfo
|
|
60
|
+
pass
|
|
61
|
+
else:
|
|
62
|
+
# Filter out unserializable objects from json_schema_extra
|
|
63
|
+
if isinstance(meta.value, type):
|
|
64
|
+
# Skip type objects - can't be serialized
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
# Any other metadata goes in json_schema_extra
|
|
68
|
+
if "json_schema_extra" not in field_kwargs:
|
|
69
|
+
field_kwargs["json_schema_extra"] = {}
|
|
70
|
+
field_kwargs["json_schema_extra"][meta.key] = meta.value
|
|
71
|
+
|
|
72
|
+
# Handle nullable case - ensure default is set if not already
|
|
73
|
+
if (
|
|
74
|
+
spec.is_nullable
|
|
75
|
+
and "default" not in field_kwargs
|
|
76
|
+
and "default_factory" not in field_kwargs
|
|
77
|
+
):
|
|
78
|
+
field_kwargs["default"] = None
|
|
79
|
+
|
|
80
|
+
field_info = PydanticField(**field_kwargs)
|
|
81
|
+
field_info.annotation = spec.annotation
|
|
82
|
+
|
|
83
|
+
return field_info
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def create_validator(cls, spec: "Spec") -> dict[str, Any] | None:
|
|
87
|
+
"""Create Pydantic field_validator from Spec metadata."""
|
|
88
|
+
from .._sentinel import Undefined
|
|
89
|
+
|
|
90
|
+
v = spec.get("validator")
|
|
91
|
+
if v is Unset or v is Undefined:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
from pydantic import field_validator
|
|
95
|
+
|
|
96
|
+
field_name = spec.name or "field"
|
|
97
|
+
# check_fields=False allows the validator to be defined in a base class before the field exists
|
|
98
|
+
return {f"{field_name}_validator": field_validator(field_name, check_fields=False)(v)}
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def create_model(
|
|
102
|
+
cls,
|
|
103
|
+
op: "Operable",
|
|
104
|
+
model_name: str,
|
|
105
|
+
include: set[str] | None = None,
|
|
106
|
+
exclude: set[str] | None = None,
|
|
107
|
+
base_type: type["BaseModel"] | None = None,
|
|
108
|
+
doc: str | None = None,
|
|
109
|
+
) -> type["BaseModel"]:
|
|
110
|
+
"""Generate Pydantic BaseModel from Operable using pydantic.create_model()."""
|
|
111
|
+
from pydantic import BaseModel, create_model
|
|
112
|
+
|
|
113
|
+
use_specs = op.get_specs(include=include, exclude=exclude)
|
|
114
|
+
use_fields = {i.name: cls.create_field(i) for i in use_specs if i.name}
|
|
115
|
+
|
|
116
|
+
# Convert fields to (type, FieldInfo) tuples for create_model
|
|
117
|
+
field_definitions = {
|
|
118
|
+
name: (field_info.annotation, field_info) for name, field_info in use_fields.items()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Collect validators
|
|
122
|
+
validators = {}
|
|
123
|
+
for spec in use_specs:
|
|
124
|
+
if spec.name and (validator := cls.create_validator(spec)):
|
|
125
|
+
validators.update(validator)
|
|
126
|
+
|
|
127
|
+
# If we have validators, create a base class with them
|
|
128
|
+
# Otherwise use the provided base_type or BaseModel
|
|
129
|
+
if validators:
|
|
130
|
+
# Create a temporary base class with validators as class attributes
|
|
131
|
+
base_with_validators = type(
|
|
132
|
+
f"{model_name}Base",
|
|
133
|
+
(base_type or BaseModel,),
|
|
134
|
+
validators,
|
|
135
|
+
)
|
|
136
|
+
actual_base = base_with_validators
|
|
137
|
+
else:
|
|
138
|
+
actual_base = base_type or BaseModel
|
|
139
|
+
|
|
140
|
+
# Create model using pydantic's create_model
|
|
141
|
+
model_cls = create_model(
|
|
142
|
+
model_name,
|
|
143
|
+
__base__=actual_base,
|
|
144
|
+
__doc__=doc,
|
|
145
|
+
**field_definitions,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
model_cls.model_rebuild()
|
|
149
|
+
return model_cls
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def fuzzy_match_fields(
|
|
153
|
+
cls, data: dict, model_cls: type["BaseModel"], strict: bool = False
|
|
154
|
+
) -> dict[str, Any]:
|
|
155
|
+
"""Match data keys to Pydantic fields (fuzzy). Filters sentinels. Args: data, model_cls, strict."""
|
|
156
|
+
from lionherd_core.ln._fuzzy_match import fuzzy_match_keys
|
|
157
|
+
|
|
158
|
+
from .._sentinel import not_sentinel
|
|
159
|
+
|
|
160
|
+
# "ignore" mode only includes successfully matched fields (no sentinel injection)
|
|
161
|
+
# "raise" mode raises on unmatched keys for strict validation
|
|
162
|
+
handle_mode = "raise" if strict else "ignore"
|
|
163
|
+
|
|
164
|
+
matched = fuzzy_match_keys(data, model_cls.model_fields, handle_unmatched=handle_mode)
|
|
165
|
+
|
|
166
|
+
# Filter out sentinel values (Unset, Undefined)
|
|
167
|
+
return {k: v for k, v in matched.items() if not_sentinel(v)}
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def validate_model(cls, model_cls: type["BaseModel"], data: dict) -> "BaseModel":
|
|
171
|
+
"""Validate dict → Pydantic model via model_validate()."""
|
|
172
|
+
return model_cls.model_validate(data)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def dump_model(cls, instance: "BaseModel") -> dict[str, Any]:
|
|
176
|
+
"""Dump Pydantic model → dict via model_dump()."""
|
|
177
|
+
return instance.model_dump()
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lionherd-core
|
|
3
|
+
Version: 1.0.0a3
|
|
4
|
+
Summary: The kernel layer for production AI agents - protocol-based, type-safe, zero framework lock-in
|
|
5
|
+
Project-URL: Homepage, https://github.com/khive-ai/lionherd-core
|
|
6
|
+
Project-URL: Documentation, https://github.com/khive-ai/lionherd-core/blob/main/README.md
|
|
7
|
+
Project-URL: Repository, https://github.com/khive-ai/lionherd-core
|
|
8
|
+
Project-URL: Issues, https://github.com/khive-ai/lionherd-core/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/khive-ai/lionherd-core/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: HaiyangLi <quantocean.li@gmail.com>
|
|
11
|
+
License: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agents,ai,async,llm,multi-agent,orchestration,protocols,type-safety
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: anyio>=4.7.0
|
|
26
|
+
Requires-Dist: pydapter>=1.2.0
|
|
27
|
+
Provides-Extra: schema-gen
|
|
28
|
+
Requires-Dist: datamodel-code-generator>=0.25.0; extra == 'schema-gen'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# lionherd-core
|
|
32
|
+
|
|
33
|
+
> The kernel layer for production AI agents
|
|
34
|
+
|
|
35
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
[](https://codecov.io/github/khive-ai/lionherd-core)
|
|
38
|
+
[](https://pypi.org/project/lionherd-core/)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why lionherd-core
|
|
43
|
+
|
|
44
|
+
Zero framework lock-in. Use what you need, ignore the rest. Build production AI
|
|
45
|
+
systems your way.
|
|
46
|
+
|
|
47
|
+
- ✅ **Protocol-based architecture** (Rust-inspired) - compose capabilities
|
|
48
|
+
without inheritance hell
|
|
49
|
+
- ✅ **Type-safe runtime validation** (Pydantic V2) - catch bugs before they
|
|
50
|
+
bite
|
|
51
|
+
- ✅ **Async-first** with thread-safe operations - scale without tears
|
|
52
|
+
- ✅ **99% test coverage** - production-ready from day one
|
|
53
|
+
- ✅ **Minimal dependencies** (pydapter + anyio) - no dependency hell
|
|
54
|
+
|
|
55
|
+
lionherd-core gives you composable primitives that work exactly how you want
|
|
56
|
+
them to.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## When to use this
|
|
61
|
+
|
|
62
|
+
### Perfect for
|
|
63
|
+
|
|
64
|
+
1. **Multi-agent orchestration**
|
|
65
|
+
- Define workflow DAGs with conditional edges
|
|
66
|
+
- Type-safe agent state management
|
|
67
|
+
- Protocol-based capability composition
|
|
68
|
+
|
|
69
|
+
2. **Structured LLM outputs**
|
|
70
|
+
- Parse messy LLM responses → validated Python objects
|
|
71
|
+
- Fuzzy parsing tolerates formatting variations
|
|
72
|
+
- Declarative schemas with Pydantic integration
|
|
73
|
+
|
|
74
|
+
3. **Production AI systems**
|
|
75
|
+
- Thread-safe collections for concurrent operations
|
|
76
|
+
- Async-first architecture scales naturally
|
|
77
|
+
- Protocol system enables clean interfaces
|
|
78
|
+
|
|
79
|
+
4. **Custom AI frameworks**
|
|
80
|
+
- Build your own framework on solid primitives
|
|
81
|
+
- Protocol composition beats inheritance
|
|
82
|
+
- Adapter pattern for storage/serialization flexibility
|
|
83
|
+
|
|
84
|
+
### Not for
|
|
85
|
+
|
|
86
|
+
- Quick prototypes (try LangChain)
|
|
87
|
+
- Learning AI agents (too low-level)
|
|
88
|
+
- No-code solutions (this is code-first)
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install lionherd-core
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Requirements**: Python ≥3.11
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Quick Examples
|
|
103
|
+
|
|
104
|
+
### 1. Type-Safe Agent Collections
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from lionherd_core import Element, Pile
|
|
108
|
+
from uuid import uuid4
|
|
109
|
+
|
|
110
|
+
class Agent(Element):
|
|
111
|
+
name: str
|
|
112
|
+
role: str
|
|
113
|
+
status: str = "idle"
|
|
114
|
+
|
|
115
|
+
# Type-safe collection
|
|
116
|
+
agents = Pile[Agent](item_type=Agent)
|
|
117
|
+
researcher = Agent(id=uuid4(), name="Alice", role="researcher")
|
|
118
|
+
agents.include(researcher)
|
|
119
|
+
|
|
120
|
+
# O(1) UUID lookup
|
|
121
|
+
found = agents[researcher.id]
|
|
122
|
+
|
|
123
|
+
# Predicate queries
|
|
124
|
+
idle_agents = agents.get(lambda a: a.status == "idle")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. Directed Graphs
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from lionherd_core import Graph, Node, Edge
|
|
131
|
+
|
|
132
|
+
graph = Graph()
|
|
133
|
+
|
|
134
|
+
# Add nodes
|
|
135
|
+
research = Node(content="Research")
|
|
136
|
+
analyze = Node(content="Analyze")
|
|
137
|
+
report = Node(content="Report")
|
|
138
|
+
|
|
139
|
+
graph.add_node(research)
|
|
140
|
+
graph.add_node(analyze)
|
|
141
|
+
graph.add_node(report)
|
|
142
|
+
|
|
143
|
+
# Define execution flow with edges
|
|
144
|
+
graph.add_edge(Edge(head=research.id, tail=analyze.id))
|
|
145
|
+
graph.add_edge(Edge(head=analyze.id, tail=report.id))
|
|
146
|
+
|
|
147
|
+
# Traverse graph
|
|
148
|
+
current = research
|
|
149
|
+
while current:
|
|
150
|
+
print(f"Executing: {current.content}")
|
|
151
|
+
successors = graph.get_successors(current.id)
|
|
152
|
+
current = successors[0] if successors else None
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 3. Structured LLM Outputs (LNDL)
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from lionherd_core import Spec, Operable
|
|
159
|
+
from lionherd_core.lndl import parse_lndl_fuzzy
|
|
160
|
+
from pydantic import BaseModel
|
|
161
|
+
|
|
162
|
+
class Research(BaseModel):
|
|
163
|
+
query: str
|
|
164
|
+
findings: list[str]
|
|
165
|
+
confidence: float = 0.8
|
|
166
|
+
|
|
167
|
+
# Define schema
|
|
168
|
+
operable = Operable([Spec(Research, name="research")])
|
|
169
|
+
|
|
170
|
+
# Parse LLM output (tolerates typos and formatting variations)
|
|
171
|
+
llm_response = """
|
|
172
|
+
<lvar Research.query q>AI architectures</lvar>
|
|
173
|
+
<lvar Research.findings f>["Protocol-based", "Async-first"]</lvar>
|
|
174
|
+
<lvar Research.confidence c>0.92</lvar>
|
|
175
|
+
OUT{research: [q, f, c]}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
result = parse_lndl_fuzzy(llm_response, operable)
|
|
179
|
+
print(result.research.confidence) # 0.92
|
|
180
|
+
print(result.research.query) # "AI architectures"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4. Protocol-Based Design
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from lionherd_core.protocols import Observable, Serializable, Adaptable
|
|
187
|
+
|
|
188
|
+
# Check capabilities at runtime
|
|
189
|
+
if isinstance(obj, Observable):
|
|
190
|
+
print(obj.id) # UUID guaranteed
|
|
191
|
+
|
|
192
|
+
if isinstance(obj, Serializable):
|
|
193
|
+
data = obj.to_dict() # Serialization guaranteed
|
|
194
|
+
|
|
195
|
+
# Compose capabilities without inheritance
|
|
196
|
+
from lionherd_core.protocols import implements
|
|
197
|
+
|
|
198
|
+
@implements(Observable, Serializable, Adaptable)
|
|
199
|
+
class CustomAgent:
|
|
200
|
+
def __init__(self):
|
|
201
|
+
self.id = uuid4()
|
|
202
|
+
|
|
203
|
+
def to_dict(self):
|
|
204
|
+
return {"id": str(self.id)}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Core Components
|
|
210
|
+
|
|
211
|
+
| Component | Purpose | Use When |
|
|
212
|
+
|-----------|---------|----------|
|
|
213
|
+
| **Element** | UUID + metadata | You need unique identity |
|
|
214
|
+
| **Node** | Polymorphic content | You need flexible content storage |
|
|
215
|
+
| **Pile[T]** | Type-safe collections | You need thread-safe typed collections |
|
|
216
|
+
| **Graph** | Directed graph with edges | You need workflow DAGs |
|
|
217
|
+
| **Flow** | Pile of progressions + items | You need ordered sequences |
|
|
218
|
+
| **Progression** | Ordered UUID sequence | You need to track execution order |
|
|
219
|
+
| **LNDL** | LLM output parser | You need structured LLM outputs |
|
|
220
|
+
|
|
221
|
+
### Protocols (Rust-Inspired)
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from lionherd_core.protocols import (
|
|
225
|
+
Observable, # UUID + metadata
|
|
226
|
+
Serializable, # to_dict(), to_json()
|
|
227
|
+
Deserializable, # from_dict()
|
|
228
|
+
Adaptable, # Multi-format conversion
|
|
229
|
+
AsyncAdaptable, # Async adaptation
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Why protocols?**
|
|
234
|
+
|
|
235
|
+
- Structural typing beats inheritance
|
|
236
|
+
- Runtime checks with `isinstance()`
|
|
237
|
+
- Compose capabilities à la carte
|
|
238
|
+
- Zero performance overhead
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Use Cases in Detail
|
|
243
|
+
|
|
244
|
+
### Multi-Agent Systems
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
# Define agent types with protocols
|
|
248
|
+
class ResearchAgent(Element):
|
|
249
|
+
expertise: str
|
|
250
|
+
status: str
|
|
251
|
+
|
|
252
|
+
class AnalystAgent(Element):
|
|
253
|
+
domain: str
|
|
254
|
+
status: str
|
|
255
|
+
|
|
256
|
+
# Type-safe agent registry
|
|
257
|
+
researchers = Pile[ResearchAgent](item_type=ResearchAgent)
|
|
258
|
+
analysts = Pile[AnalystAgent](item_type=AnalystAgent)
|
|
259
|
+
|
|
260
|
+
# Workflow orchestration with Graph
|
|
261
|
+
workflow = Graph()
|
|
262
|
+
research_phase = Node(content="research")
|
|
263
|
+
analysis_phase = Node(content="analysis")
|
|
264
|
+
|
|
265
|
+
workflow.add_node(research_phase)
|
|
266
|
+
workflow.add_node(analysis_phase)
|
|
267
|
+
workflow.add_edge(Edge(head=research_phase.id, tail=analysis_phase.id))
|
|
268
|
+
|
|
269
|
+
# Execute with conditional branching
|
|
270
|
+
current = research_phase
|
|
271
|
+
while current:
|
|
272
|
+
# Dispatch to appropriate agents
|
|
273
|
+
if current.content == "research":
|
|
274
|
+
execute_research(researchers)
|
|
275
|
+
elif current.content == "analysis":
|
|
276
|
+
execute_analysis(analysts)
|
|
277
|
+
|
|
278
|
+
# Progress workflow
|
|
279
|
+
successors = workflow.get_successors(current.id)
|
|
280
|
+
current = successors[0] if successors else None
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Tool Calling & Function Execution
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from lionherd_core import Element, Pile, Spec, Operable
|
|
287
|
+
from lionherd_core.lndl import parse_lndl_fuzzy
|
|
288
|
+
from collections.abc import Callable
|
|
289
|
+
from pydantic import BaseModel
|
|
290
|
+
from typing import Any
|
|
291
|
+
|
|
292
|
+
class Tool(Element):
|
|
293
|
+
name: str
|
|
294
|
+
description: str
|
|
295
|
+
func: Callable[..., Any]
|
|
296
|
+
|
|
297
|
+
class Config:
|
|
298
|
+
arbitrary_types_allowed = True
|
|
299
|
+
|
|
300
|
+
# Tool registry
|
|
301
|
+
tools = Pile[Tool](item_type=Tool)
|
|
302
|
+
tools.include([
|
|
303
|
+
Tool(name="search", description="Search web", func=search_fn),
|
|
304
|
+
Tool(name="calculate", description="Math ops", func=calc_fn),
|
|
305
|
+
])
|
|
306
|
+
|
|
307
|
+
# Parse LLM tool call
|
|
308
|
+
class ToolCall(BaseModel):
|
|
309
|
+
tool: str
|
|
310
|
+
args: dict
|
|
311
|
+
|
|
312
|
+
operable = Operable([Spec(ToolCall, name="call")])
|
|
313
|
+
|
|
314
|
+
llm_output = """
|
|
315
|
+
<lvar ToolCall.tool t>search</lvar>
|
|
316
|
+
<lvar ToolCall.args a>{"query": "AI agents"}</lvar>
|
|
317
|
+
OUT{call: [t, a]}
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
parsed = parse_lndl_fuzzy(llm_output, operable)
|
|
321
|
+
|
|
322
|
+
# Execute
|
|
323
|
+
tool = tools.get(lambda t: t.name == parsed.call.tool)[0]
|
|
324
|
+
result = tool.func(**parsed.call.args)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Memory Systems
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from lionherd_core import Node, Graph, Edge
|
|
331
|
+
from datetime import datetime
|
|
332
|
+
|
|
333
|
+
class Memory(Node):
|
|
334
|
+
timestamp: datetime
|
|
335
|
+
importance: float
|
|
336
|
+
tags: list[str]
|
|
337
|
+
|
|
338
|
+
# Memory graph (semantic connections)
|
|
339
|
+
memory_graph = Graph()
|
|
340
|
+
|
|
341
|
+
# Add memories
|
|
342
|
+
mem1 = Memory(
|
|
343
|
+
content="User likes Python",
|
|
344
|
+
timestamp=datetime.now(),
|
|
345
|
+
importance=0.9,
|
|
346
|
+
tags=["preference"]
|
|
347
|
+
)
|
|
348
|
+
mem2 = Memory(
|
|
349
|
+
content="User dislikes Java",
|
|
350
|
+
timestamp=datetime.now(),
|
|
351
|
+
importance=0.7,
|
|
352
|
+
tags=["preference"]
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
memory_graph.add_node(mem1)
|
|
356
|
+
memory_graph.add_node(mem2)
|
|
357
|
+
|
|
358
|
+
# Connect related memories
|
|
359
|
+
memory_graph.add_edge(Edge(head=mem1.id, tail=mem2.id, label=["preference"]))
|
|
360
|
+
|
|
361
|
+
# Query by importance
|
|
362
|
+
important_memories = memory_graph.nodes.get(lambda m: m.importance > 0.8)
|
|
363
|
+
|
|
364
|
+
# Traverse connections
|
|
365
|
+
related = memory_graph.get_successors(mem1.id)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### RAG Pipelines
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from lionherd_core import Pile, Element
|
|
372
|
+
|
|
373
|
+
class Document(Element):
|
|
374
|
+
content: str
|
|
375
|
+
embedding: list[float]
|
|
376
|
+
metadata: dict
|
|
377
|
+
|
|
378
|
+
# Document store
|
|
379
|
+
docs = Pile[Document](item_type=Document)
|
|
380
|
+
|
|
381
|
+
# Add documents with embeddings
|
|
382
|
+
doc = Document(
|
|
383
|
+
content="Protocol-based design enables...",
|
|
384
|
+
embedding=get_embedding(content),
|
|
385
|
+
metadata={"source": "paper.pdf", "page": 12}
|
|
386
|
+
)
|
|
387
|
+
docs.include(doc)
|
|
388
|
+
|
|
389
|
+
# Retrieve by predicate
|
|
390
|
+
results = docs.get(lambda d: d.metadata["source"] == "paper.pdf")
|
|
391
|
+
|
|
392
|
+
# Integrate with vector DB via adapters
|
|
393
|
+
doc_dict = doc.to_dict()
|
|
394
|
+
vector_db.insert(doc_dict)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Architecture
|
|
400
|
+
|
|
401
|
+
```text
|
|
402
|
+
Your Application
|
|
403
|
+
↓
|
|
404
|
+
lionherd-core ← You are here
|
|
405
|
+
├── Protocols (Observable, Serializable, Adaptable)
|
|
406
|
+
├── Base Classes (Element, Node, Pile, Graph, Flow)
|
|
407
|
+
├── LNDL Parser (LLM output → Python objects)
|
|
408
|
+
└── Utilities (async, serialization, adapters)
|
|
409
|
+
↓
|
|
410
|
+
Python Ecosystem (Pydantic, asyncio, pydapter)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Design Principles:**
|
|
414
|
+
|
|
415
|
+
1. **Protocols over inheritance** - Compose capabilities structurally
|
|
416
|
+
2. **Operations as morphisms** - Preserve semantics through composition
|
|
417
|
+
3. **Async-first** - Native asyncio with thread-safe operations
|
|
418
|
+
4. **Isolated adapters** - Per-class registries, zero pollution
|
|
419
|
+
5. **Minimal dependencies** - Only pydapter + anyio
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Development
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Setup
|
|
427
|
+
git clone https://github.com/khive-ai/lionherd-core.git
|
|
428
|
+
cd lionherd-core
|
|
429
|
+
uv sync --all-extras
|
|
430
|
+
|
|
431
|
+
# Test
|
|
432
|
+
uv run pytest --cov=lionherd_core
|
|
433
|
+
|
|
434
|
+
# Lint
|
|
435
|
+
uv run ruff check .
|
|
436
|
+
uv run ruff format .
|
|
437
|
+
|
|
438
|
+
# Type check
|
|
439
|
+
uv run mypy src/
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Test Coverage**: Maintained at 99%+ with comprehensive test suite
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Roadmap
|
|
447
|
+
|
|
448
|
+
### v1.0.0-beta (Q1 2025)
|
|
449
|
+
|
|
450
|
+
- API stabilization
|
|
451
|
+
- Comprehensive docs
|
|
452
|
+
- Performance benchmarks
|
|
453
|
+
- Additional adapters (Protobuf, MessagePack)
|
|
454
|
+
|
|
455
|
+
### v1.0.0 (Q2 2025)
|
|
456
|
+
|
|
457
|
+
- Frozen public API
|
|
458
|
+
- Production-hardened
|
|
459
|
+
- Ecosystem integrations
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Related Projects
|
|
464
|
+
|
|
465
|
+
Part of the Lion ecosystem:
|
|
466
|
+
|
|
467
|
+
- **[lionagi](https://github.com/khive-ai/lionagi)**: v0 of the Lion ecosystem
|
|
468
|
+
- full agentic AI framework with advanced orchestration capabilities
|
|
469
|
+
- **[pydapter](https://github.com/khive-ai/pydapter)**: Universal data adapter
|
|
470
|
+
(JSON/YAML/TOML/SQL/Neo4j/Redis/MongoDB/Weaviate/etc.)
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
Apache 2.0 - Free for commercial use, no strings attached.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Support
|
|
481
|
+
|
|
482
|
+
- [GitHub Issues](https://github.com/khive-ai/lionherd-core/issues)
|
|
483
|
+
- [Discussions](https://github.com/khive-ai/lionherd-core/discussions)
|
|
484
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Created by
|
|
489
|
+
|
|
490
|
+
**[HaiyangLi (Ocean)](https://github.com/ohdearquant)** - [khive.ai](https://khive.ai)
|
|
491
|
+
|
|
492
|
+
Inspired by Rust traits, Pydantic validation, and functional programming.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
**Ready to build?**
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
pip install lionherd-core
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
*Alpha release - APIs may evolve. Feedback shapes the future.*
|