rotalabs-context 1.0.0__tar.gz
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.
- rotalabs_context-1.0.0/.coverage +0 -0
- rotalabs_context-1.0.0/LICENSE +3 -0
- rotalabs_context-1.0.0/PKG-INFO +126 -0
- rotalabs_context-1.0.0/README.md +81 -0
- rotalabs_context-1.0.0/pyproject.toml +86 -0
- rotalabs_context-1.0.0/src/rotalabs_context/__init__.py +36 -0
- rotalabs_context-1.0.0/src/rotalabs_context/_telemetry.py +34 -0
- rotalabs_context-1.0.0/src/rotalabs_context/_version.py +1 -0
- rotalabs_context-1.0.0/src/rotalabs_context/adapters/__init__.py +1 -0
- rotalabs_context-1.0.0/src/rotalabs_context/adapters/airflow.py +81 -0
- rotalabs_context-1.0.0/src/rotalabs_context/backends/__init__.py +1 -0
- rotalabs_context-1.0.0/src/rotalabs_context/backends/base.py +34 -0
- rotalabs_context-1.0.0/src/rotalabs_context/backends/memory.py +136 -0
- rotalabs_context-1.0.0/src/rotalabs_context/backends/platform.py +99 -0
- rotalabs_context-1.0.0/src/rotalabs_context/engine.py +247 -0
- rotalabs_context-1.0.0/src/rotalabs_context/models.py +99 -0
- rotalabs_context-1.0.0/tests/__init__.py +0 -0
- rotalabs_context-1.0.0/tests/test_engine.py +67 -0
- rotalabs_context-1.0.0/uv.lock +6605 -0
|
Binary file
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rotalabs-context
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Context intelligence for AI agents - Ingest, search, and subscribe to shared context across your AI systems
|
|
5
|
+
Project-URL: Homepage, https://rotalabs.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/rotalabs/rotalabs-context
|
|
7
|
+
Project-URL: Documentation, https://rotalabs.ai/docs/context
|
|
8
|
+
Author-email: Subhadip Mitra <subhadip@rotalabs.ai>, Rotalabs Research <research@rotalabs.ai>
|
|
9
|
+
License-Expression: AGPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-agents,context,context-engine,embeddings,etl,knowledge-graph,rag,semantic-search
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Provides-Extra: airflow
|
|
25
|
+
Requires-Dist: apache-airflow>=2.5.0; extra == 'airflow'
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: httpx>=0.25.0; extra == 'all'
|
|
28
|
+
Requires-Dist: numpy>=1.24.0; extra == 'all'
|
|
29
|
+
Requires-Dist: rotalabs[telemetry]>=1.0.0; extra == 'all'
|
|
30
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'all'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: local
|
|
38
|
+
Requires-Dist: numpy>=1.24.0; extra == 'local'
|
|
39
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == 'local'
|
|
40
|
+
Provides-Extra: platform
|
|
41
|
+
Requires-Dist: httpx>=0.25.0; extra == 'platform'
|
|
42
|
+
Provides-Extra: telemetry
|
|
43
|
+
Requires-Dist: rotalabs[telemetry]>=1.0.0; extra == 'telemetry'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# rotalabs-context
|
|
47
|
+
|
|
48
|
+
> Context intelligence for AI agents — Ingest, search, and subscribe to shared context across your AI systems.
|
|
49
|
+
|
|
50
|
+
Part of the [Rotalabs](https://rotalabs.ai) trust intelligence research ecosystem.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install rotalabs-context
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With local embedding support:
|
|
59
|
+
```bash
|
|
60
|
+
pip install rotalabs-context[local]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With Rotascale platform integration:
|
|
64
|
+
```bash
|
|
65
|
+
pip install rotalabs-context[platform]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from rotalabs_context import ContextEngine
|
|
72
|
+
|
|
73
|
+
# Local mode (zero config, SQLite backend)
|
|
74
|
+
ctx = ContextEngine()
|
|
75
|
+
|
|
76
|
+
# Platform mode (connects to Rotascale)
|
|
77
|
+
ctx = ContextEngine(
|
|
78
|
+
api_key="rot_...",
|
|
79
|
+
base_url="https://api.rotascale.com",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Ingest context
|
|
83
|
+
result = ctx.ingest(
|
|
84
|
+
records=[
|
|
85
|
+
{"content": "User reported login issue", "title": "Support ticket #1234"},
|
|
86
|
+
{"content": "Auth service latency spike at 2pm UTC", "title": "Incident report"},
|
|
87
|
+
],
|
|
88
|
+
source="helpdesk",
|
|
89
|
+
tags=["support", "auth"],
|
|
90
|
+
scope="team:support",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Search
|
|
94
|
+
results = ctx.search("login issues", top_k=5, mode="semantic")
|
|
95
|
+
for hit in results:
|
|
96
|
+
print(f"{hit.score:.2f} — {hit.title}")
|
|
97
|
+
|
|
98
|
+
# Subscribe to context updates
|
|
99
|
+
ctx.on("context.created", filter={"tags": ["support"]}, callback=my_handler)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Features
|
|
103
|
+
|
|
104
|
+
- **Ingest**: Push data from any pipeline; we add the "C" (embeddings, entities, relationships)
|
|
105
|
+
- **Search**: Semantic + keyword + hybrid search across all ingested context
|
|
106
|
+
- **Subscribe**: Real-time context propagation via events
|
|
107
|
+
- **ACLs**: Scope-based access control (global, team, agent, project, user)
|
|
108
|
+
- **Pluggable backends**: SQLite (local), PostgreSQL (self-hosted), Rotascale Platform (managed)
|
|
109
|
+
|
|
110
|
+
## Pipeline Adapters
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Airflow
|
|
114
|
+
from rotalabs_context.adapters import AirflowContextOperator
|
|
115
|
+
|
|
116
|
+
# In your DAG
|
|
117
|
+
ingest_task = AirflowContextOperator(
|
|
118
|
+
task_id="ingest_to_context",
|
|
119
|
+
source="airflow-etl",
|
|
120
|
+
tags=["daily-batch"],
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
AGPL-3.0 — See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# rotalabs-context
|
|
2
|
+
|
|
3
|
+
> Context intelligence for AI agents — Ingest, search, and subscribe to shared context across your AI systems.
|
|
4
|
+
|
|
5
|
+
Part of the [Rotalabs](https://rotalabs.ai) trust intelligence research ecosystem.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install rotalabs-context
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With local embedding support:
|
|
14
|
+
```bash
|
|
15
|
+
pip install rotalabs-context[local]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With Rotascale platform integration:
|
|
19
|
+
```bash
|
|
20
|
+
pip install rotalabs-context[platform]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from rotalabs_context import ContextEngine
|
|
27
|
+
|
|
28
|
+
# Local mode (zero config, SQLite backend)
|
|
29
|
+
ctx = ContextEngine()
|
|
30
|
+
|
|
31
|
+
# Platform mode (connects to Rotascale)
|
|
32
|
+
ctx = ContextEngine(
|
|
33
|
+
api_key="rot_...",
|
|
34
|
+
base_url="https://api.rotascale.com",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Ingest context
|
|
38
|
+
result = ctx.ingest(
|
|
39
|
+
records=[
|
|
40
|
+
{"content": "User reported login issue", "title": "Support ticket #1234"},
|
|
41
|
+
{"content": "Auth service latency spike at 2pm UTC", "title": "Incident report"},
|
|
42
|
+
],
|
|
43
|
+
source="helpdesk",
|
|
44
|
+
tags=["support", "auth"],
|
|
45
|
+
scope="team:support",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Search
|
|
49
|
+
results = ctx.search("login issues", top_k=5, mode="semantic")
|
|
50
|
+
for hit in results:
|
|
51
|
+
print(f"{hit.score:.2f} — {hit.title}")
|
|
52
|
+
|
|
53
|
+
# Subscribe to context updates
|
|
54
|
+
ctx.on("context.created", filter={"tags": ["support"]}, callback=my_handler)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Ingest**: Push data from any pipeline; we add the "C" (embeddings, entities, relationships)
|
|
60
|
+
- **Search**: Semantic + keyword + hybrid search across all ingested context
|
|
61
|
+
- **Subscribe**: Real-time context propagation via events
|
|
62
|
+
- **ACLs**: Scope-based access control (global, team, agent, project, user)
|
|
63
|
+
- **Pluggable backends**: SQLite (local), PostgreSQL (self-hosted), Rotascale Platform (managed)
|
|
64
|
+
|
|
65
|
+
## Pipeline Adapters
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# Airflow
|
|
69
|
+
from rotalabs_context.adapters import AirflowContextOperator
|
|
70
|
+
|
|
71
|
+
# In your DAG
|
|
72
|
+
ingest_task = AirflowContextOperator(
|
|
73
|
+
task_id="ingest_to_context",
|
|
74
|
+
source="airflow-etl",
|
|
75
|
+
tags=["daily-batch"],
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
AGPL-3.0 — See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rotalabs-context"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Context intelligence for AI agents - Ingest, search, and subscribe to shared context across your AI systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "AGPL-3.0-or-later"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Subhadip Mitra", email = "subhadip@rotalabs.ai" },
|
|
14
|
+
{ name = "Rotalabs Research", email = "research@rotalabs.ai" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
]
|
|
28
|
+
keywords = [
|
|
29
|
+
"context",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"rag",
|
|
32
|
+
"embeddings",
|
|
33
|
+
"knowledge-graph",
|
|
34
|
+
"etl",
|
|
35
|
+
"context-engine",
|
|
36
|
+
"semantic-search",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"pydantic>=2.0.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
local = [
|
|
44
|
+
"numpy>=1.24.0",
|
|
45
|
+
"sentence-transformers>=2.2.0",
|
|
46
|
+
]
|
|
47
|
+
platform = [
|
|
48
|
+
"httpx>=0.25.0",
|
|
49
|
+
]
|
|
50
|
+
airflow = [
|
|
51
|
+
"apache-airflow>=2.5.0",
|
|
52
|
+
]
|
|
53
|
+
telemetry = ["rotalabs[telemetry]>=1.0.0"]
|
|
54
|
+
dev = [
|
|
55
|
+
"pytest>=7.4.0",
|
|
56
|
+
"pytest-cov>=4.1.0",
|
|
57
|
+
"pytest-asyncio>=0.21.0",
|
|
58
|
+
"ruff>=0.1.0",
|
|
59
|
+
"mypy>=1.5.0",
|
|
60
|
+
]
|
|
61
|
+
all = ["rotalabs-context[local,platform,telemetry]"]
|
|
62
|
+
|
|
63
|
+
[project.urls]
|
|
64
|
+
Homepage = "https://rotalabs.ai"
|
|
65
|
+
Repository = "https://github.com/rotalabs/rotalabs-context"
|
|
66
|
+
Documentation = "https://rotalabs.ai/docs/context"
|
|
67
|
+
|
|
68
|
+
[tool.hatch.build.targets.wheel]
|
|
69
|
+
packages = ["src/rotalabs_context"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 100
|
|
73
|
+
target-version = "py39"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
77
|
+
ignore = ["E501", "B008"]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.isort]
|
|
80
|
+
known-first-party = ["rotalabs_context"]
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests"]
|
|
84
|
+
python_files = ["test_*.py"]
|
|
85
|
+
addopts = "-v --cov=src/rotalabs_context --cov-report=term-missing"
|
|
86
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
rotalabs-context: Context intelligence for AI agents.
|
|
3
|
+
|
|
4
|
+
Provides tools to ingest, search, and subscribe to shared context
|
|
5
|
+
across AI systems. Works standalone (local mode) or connected to
|
|
6
|
+
the Rotascale platform (managed mode).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from rotalabs_context import ContextEngine
|
|
10
|
+
|
|
11
|
+
ctx = ContextEngine()
|
|
12
|
+
ctx.ingest(records=[{"content": "hello"}], source="test")
|
|
13
|
+
results = ctx.search("hello")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from rotalabs_context._version import __version__
|
|
17
|
+
from rotalabs_context.engine import ContextEngine
|
|
18
|
+
from rotalabs_context.models import (
|
|
19
|
+
ContextEntry,
|
|
20
|
+
IngestResult,
|
|
21
|
+
SearchHit,
|
|
22
|
+
SearchResult,
|
|
23
|
+
Scope,
|
|
24
|
+
Sensitivity,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"__version__",
|
|
29
|
+
"ContextEngine",
|
|
30
|
+
"ContextEntry",
|
|
31
|
+
"IngestResult",
|
|
32
|
+
"SearchHit",
|
|
33
|
+
"SearchResult",
|
|
34
|
+
"Scope",
|
|
35
|
+
"Sensitivity",
|
|
36
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Telemetry integration for rotalabs-context.
|
|
2
|
+
|
|
3
|
+
Safe wrapper around the rotalabs telemetry system.
|
|
4
|
+
Opt-in only, gracefully degrades if not configured.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from rotalabs._telemetry import is_enabled, report_event
|
|
11
|
+
except ImportError:
|
|
12
|
+
def report_event(*args: Any, **kwargs: Any) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def is_enabled() -> bool:
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def report_context_init(backend: str) -> None:
|
|
20
|
+
if not is_enabled():
|
|
21
|
+
return
|
|
22
|
+
report_event("context.engine.init", {"backend": backend})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def report_context_ingest(source: str, count: int) -> None:
|
|
26
|
+
if not is_enabled():
|
|
27
|
+
return
|
|
28
|
+
report_event("context.ingest", {"source": source, "count": count})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def report_context_search(mode: str, total: int) -> None:
|
|
32
|
+
if not is_enabled():
|
|
33
|
+
return
|
|
34
|
+
report_event("context.search", {"mode": mode, "total": total})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pipeline adapters for integrating context into existing ETL workflows."""
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Apache Airflow adapter for rotalabs-context.
|
|
2
|
+
|
|
3
|
+
Provides an Airflow operator that ingests context during DAG execution.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from rotalabs_context.adapters.airflow import AirflowContextOperator
|
|
7
|
+
|
|
8
|
+
ingest_task = AirflowContextOperator(
|
|
9
|
+
task_id="ingest_context",
|
|
10
|
+
source="my-etl-pipeline",
|
|
11
|
+
records_callable=lambda: [{"content": "data", "title": "record"}],
|
|
12
|
+
tags=["daily", "production"],
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from airflow.models import BaseOperator
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Stub for when airflow is not installed
|
|
24
|
+
class BaseOperator: # type: ignore[no-redef]
|
|
25
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def execute(self, context: Any) -> Any:
|
|
29
|
+
raise ImportError("apache-airflow is required. Install with: pip install rotalabs-context[airflow]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AirflowContextOperator(BaseOperator):
|
|
33
|
+
"""Airflow operator that ingests records into the context engine.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
source : str
|
|
38
|
+
Source identifier for the ingested context.
|
|
39
|
+
records_callable : callable
|
|
40
|
+
Function that returns a list of record dicts.
|
|
41
|
+
tags : list[str]
|
|
42
|
+
Tags to apply.
|
|
43
|
+
scope : str
|
|
44
|
+
Visibility scope.
|
|
45
|
+
api_key : str, optional
|
|
46
|
+
Rotascale API key. If not provided, uses ROTASCALE_API_KEY env var.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
source: str,
|
|
52
|
+
records_callable: Callable[..., list[dict[str, Any]]],
|
|
53
|
+
tags: list[str] | None = None,
|
|
54
|
+
scope: str = "global",
|
|
55
|
+
api_key: str | None = None,
|
|
56
|
+
base_url: str = "https://api.rotascale.com",
|
|
57
|
+
**kwargs: Any,
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
self.source = source
|
|
61
|
+
self.records_callable = records_callable
|
|
62
|
+
self.tags = tags or []
|
|
63
|
+
self.scope = scope
|
|
64
|
+
self.api_key = api_key
|
|
65
|
+
self.base_url = base_url
|
|
66
|
+
|
|
67
|
+
def execute(self, context: Any) -> dict[str, Any]:
|
|
68
|
+
import os
|
|
69
|
+
from rotalabs_context import ContextEngine
|
|
70
|
+
|
|
71
|
+
api_key = self.api_key or os.environ.get("ROTASCALE_API_KEY")
|
|
72
|
+
ctx = ContextEngine(api_key=api_key, base_url=self.base_url)
|
|
73
|
+
|
|
74
|
+
records = self.records_callable()
|
|
75
|
+
result = ctx.ingest(
|
|
76
|
+
records=records,
|
|
77
|
+
source=self.source,
|
|
78
|
+
tags=self.tags,
|
|
79
|
+
scope=self.scope,
|
|
80
|
+
)
|
|
81
|
+
return result.model_dump()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pluggable storage backends for rotalabs-context."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Abstract backend interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
|
|
7
|
+
from rotalabs_context.models import IngestRecord, IngestResult, SearchResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Backend(abc.ABC):
|
|
11
|
+
"""Abstract storage backend for the context engine."""
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def ingest(
|
|
15
|
+
self,
|
|
16
|
+
records: list[IngestRecord],
|
|
17
|
+
source: str,
|
|
18
|
+
tags: list[str],
|
|
19
|
+
scope: str,
|
|
20
|
+
sensitivity: str,
|
|
21
|
+
generate_embeddings: bool,
|
|
22
|
+
extract_entities: bool,
|
|
23
|
+
) -> IngestResult:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def search(
|
|
28
|
+
self,
|
|
29
|
+
query: str,
|
|
30
|
+
top_k: int,
|
|
31
|
+
mode: str,
|
|
32
|
+
filters: dict,
|
|
33
|
+
) -> SearchResult:
|
|
34
|
+
...
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""In-memory backend for local development and testing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from rotalabs_context.backends.base import Backend
|
|
11
|
+
from rotalabs_context.models import (
|
|
12
|
+
ContextEntry,
|
|
13
|
+
IngestRecord,
|
|
14
|
+
IngestResult,
|
|
15
|
+
SearchHit,
|
|
16
|
+
SearchResult,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryBackend(Backend):
|
|
21
|
+
"""In-memory context store. Data is lost when process exits.
|
|
22
|
+
|
|
23
|
+
Good for testing, prototyping, and local development.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._entries: list[ContextEntry] = []
|
|
28
|
+
self._counter = 0
|
|
29
|
+
|
|
30
|
+
def ingest(
|
|
31
|
+
self,
|
|
32
|
+
records: list[IngestRecord],
|
|
33
|
+
source: str,
|
|
34
|
+
tags: list[str],
|
|
35
|
+
scope: str,
|
|
36
|
+
sensitivity: str,
|
|
37
|
+
generate_embeddings: bool,
|
|
38
|
+
extract_entities: bool,
|
|
39
|
+
) -> IngestResult:
|
|
40
|
+
entry_ids: list[str] = []
|
|
41
|
+
for record in records:
|
|
42
|
+
self._counter += 1
|
|
43
|
+
entry_id = f"ctx_{self._counter:06d}"
|
|
44
|
+
content_hash = hashlib.sha256(record.content.encode()).hexdigest()
|
|
45
|
+
|
|
46
|
+
entry = ContextEntry(
|
|
47
|
+
id=entry_id,
|
|
48
|
+
source=source,
|
|
49
|
+
source_id=record.source_id,
|
|
50
|
+
content_type=record.content_type,
|
|
51
|
+
title=record.title,
|
|
52
|
+
content=record.content,
|
|
53
|
+
content_hash=content_hash,
|
|
54
|
+
tags=tags,
|
|
55
|
+
scope=scope,
|
|
56
|
+
sensitivity=sensitivity,
|
|
57
|
+
metadata=record.metadata,
|
|
58
|
+
created_at=datetime.now(timezone.utc),
|
|
59
|
+
)
|
|
60
|
+
self._entries.append(entry)
|
|
61
|
+
entry_ids.append(entry_id)
|
|
62
|
+
|
|
63
|
+
return IngestResult(
|
|
64
|
+
entries_created=len(records),
|
|
65
|
+
embeddings_generated=0, # No embeddings in memory mode
|
|
66
|
+
entities_extracted=0,
|
|
67
|
+
entry_ids=entry_ids,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def search(
|
|
71
|
+
self,
|
|
72
|
+
query: str,
|
|
73
|
+
top_k: int,
|
|
74
|
+
mode: str,
|
|
75
|
+
filters: dict,
|
|
76
|
+
) -> SearchResult:
|
|
77
|
+
start = time.monotonic()
|
|
78
|
+
query_lower = query.lower()
|
|
79
|
+
query_tokens = set(re.findall(r"[a-z0-9]+", query_lower))
|
|
80
|
+
|
|
81
|
+
scored: list[SearchHit] = []
|
|
82
|
+
for entry in self._entries:
|
|
83
|
+
# Apply filters
|
|
84
|
+
if filters.get("tags"):
|
|
85
|
+
if not any(t in entry.tags for t in filters["tags"]):
|
|
86
|
+
continue
|
|
87
|
+
if filters.get("sources"):
|
|
88
|
+
if entry.source not in filters["sources"]:
|
|
89
|
+
continue
|
|
90
|
+
if filters.get("scopes"):
|
|
91
|
+
if entry.scope not in filters["scopes"]:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Keyword matching
|
|
95
|
+
text = f"{entry.title or ''} {entry.content}".lower()
|
|
96
|
+
text_tokens = set(re.findall(r"[a-z0-9]+", text))
|
|
97
|
+
|
|
98
|
+
if not query_tokens:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
overlap = query_tokens & text_tokens
|
|
102
|
+
if not overlap:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
score = len(overlap) / len(query_tokens)
|
|
106
|
+
|
|
107
|
+
# Extract highlights
|
|
108
|
+
highlights: list[str] = []
|
|
109
|
+
for sentence in re.split(r"[.!?\n]+", entry.content):
|
|
110
|
+
sentence = sentence.strip()
|
|
111
|
+
if not sentence:
|
|
112
|
+
continue
|
|
113
|
+
sent_tokens = set(re.findall(r"[a-z0-9]+", sentence.lower()))
|
|
114
|
+
if query_tokens & sent_tokens:
|
|
115
|
+
highlights.append(sentence[:200])
|
|
116
|
+
if len(highlights) >= 3:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
scored.append(SearchHit(
|
|
120
|
+
entry=entry,
|
|
121
|
+
score=round(score, 4),
|
|
122
|
+
highlights=highlights,
|
|
123
|
+
match_type="keyword",
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
scored.sort(key=lambda x: x.score, reverse=True)
|
|
127
|
+
results = scored[:top_k]
|
|
128
|
+
took_ms = round((time.monotonic() - start) * 1000, 2)
|
|
129
|
+
|
|
130
|
+
return SearchResult(
|
|
131
|
+
query=query,
|
|
132
|
+
mode=mode,
|
|
133
|
+
results=results,
|
|
134
|
+
total=len(results),
|
|
135
|
+
took_ms=took_ms,
|
|
136
|
+
)
|