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.
Binary file
@@ -0,0 +1,3 @@
1
+ GNU Affero General Public License v3.0
2
+
3
+ See https://www.gnu.org/licenses/agpl-3.0.en.html for full text.
@@ -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
+ )