rotalabs-context 1.0.0__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.
@@ -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
+ )
@@ -0,0 +1,99 @@
1
+ """Rotascale platform backend — connects to the managed API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rotalabs_context.backends.base import Backend
6
+ from rotalabs_context.models import (
7
+ ContextEntry,
8
+ IngestRecord,
9
+ IngestResult,
10
+ SearchHit,
11
+ SearchResult,
12
+ )
13
+
14
+
15
+ class PlatformBackend(Backend):
16
+ """Backend that connects to the Rotascale platform API.
17
+
18
+ Requires ``httpx`` (install with ``pip install rotalabs-context[platform]``).
19
+ """
20
+
21
+ def __init__(self, api_key: str | None, base_url: str) -> None:
22
+ try:
23
+ import httpx
24
+ except ImportError:
25
+ raise ImportError(
26
+ "httpx is required for platform mode. "
27
+ "Install with: pip install rotalabs-context[platform]"
28
+ )
29
+ self._client = httpx.Client(
30
+ base_url=base_url,
31
+ headers={
32
+ "Authorization": f"Bearer {api_key}",
33
+ "Content-Type": "application/json",
34
+ "User-Agent": "rotalabs-context/1.0.0",
35
+ },
36
+ timeout=30.0,
37
+ )
38
+
39
+ def ingest(
40
+ self,
41
+ records: list[IngestRecord],
42
+ source: str,
43
+ tags: list[str],
44
+ scope: str,
45
+ sensitivity: str,
46
+ generate_embeddings: bool,
47
+ extract_entities: bool,
48
+ ) -> IngestResult:
49
+ resp = self._client.post(
50
+ "/v1/context-engine/ingest",
51
+ json={
52
+ "records": [r.model_dump() for r in records],
53
+ "source": source,
54
+ "tags": tags,
55
+ "scope": scope,
56
+ "sensitivity": sensitivity,
57
+ "generate_embeddings": generate_embeddings,
58
+ "extract_entities": extract_entities,
59
+ },
60
+ )
61
+ resp.raise_for_status()
62
+ data = resp.json()
63
+ return IngestResult(**data)
64
+
65
+ def search(
66
+ self,
67
+ query: str,
68
+ top_k: int,
69
+ mode: str,
70
+ filters: dict,
71
+ ) -> SearchResult:
72
+ resp = self._client.post(
73
+ "/v1/context-engine/search",
74
+ json={
75
+ "query": query,
76
+ "top_k": top_k,
77
+ "mode": mode,
78
+ "filters": filters or None,
79
+ },
80
+ )
81
+ resp.raise_for_status()
82
+ data = resp.json()
83
+
84
+ results = []
85
+ for hit in data.get("results", []):
86
+ results.append(SearchHit(
87
+ entry=ContextEntry(**hit["entry"]),
88
+ score=hit["score"],
89
+ highlights=hit.get("highlights", []),
90
+ match_type=hit.get("match_type", mode),
91
+ ))
92
+
93
+ return SearchResult(
94
+ query=data["query"],
95
+ mode=data["mode"],
96
+ results=results,
97
+ total=data["total"],
98
+ took_ms=data.get("took_ms", 0),
99
+ )
@@ -0,0 +1,247 @@
1
+ """ContextEngine — main entry point for rotalabs-context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import logging
7
+ from typing import Any, Callable
8
+
9
+ from rotalabs_context.models import (
10
+ ContextEntry,
11
+ IngestRecord,
12
+ IngestResult,
13
+ SearchHit,
14
+ SearchResult,
15
+ Subscription,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ContextEngine:
22
+ """Context intelligence engine.
23
+
24
+ Provides ingest, search, and subscribe capabilities. Works in two modes:
25
+
26
+ - **Local mode** (default): Uses an in-memory or SQLite backend for
27
+ zero-config local development.
28
+ - **Platform mode**: Connects to the Rotascale platform API for
29
+ managed context intelligence with embedding generation, entity
30
+ extraction, ACLs, and real-time subscriptions.
31
+
32
+ Parameters
33
+ ----------
34
+ api_key : str, optional
35
+ Rotascale API key. If provided, enables platform mode.
36
+ base_url : str
37
+ Base URL for the Rotascale API.
38
+ backend : str
39
+ Backend to use: "memory" (default), "sqlite", or "platform".
40
+
41
+ Examples
42
+ --------
43
+ >>> ctx = ContextEngine()
44
+ >>> ctx.ingest(records=[{"content": "hello"}], source="test")
45
+ >>> results = ctx.search("hello")
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ api_key: str | None = None,
51
+ base_url: str = "https://api.rotascale.com",
52
+ backend: str | None = None,
53
+ ) -> None:
54
+ self.api_key = api_key
55
+ self.base_url = base_url.rstrip("/")
56
+ self._event_handlers: dict[str, list[dict]] = {}
57
+
58
+ # Select backend
59
+ if backend:
60
+ self._backend_type = backend
61
+ elif api_key:
62
+ self._backend_type = "platform"
63
+ else:
64
+ self._backend_type = "memory"
65
+
66
+ # Initialize backend
67
+ if self._backend_type == "platform":
68
+ from rotalabs_context.backends.platform import PlatformBackend
69
+ self._backend = PlatformBackend(api_key=api_key, base_url=self.base_url)
70
+ else:
71
+ from rotalabs_context.backends.memory import MemoryBackend
72
+ self._backend = MemoryBackend()
73
+
74
+ # Report telemetry
75
+ _report_init(self._backend_type)
76
+
77
+ def ingest(
78
+ self,
79
+ records: list[dict[str, Any]],
80
+ source: str,
81
+ tags: list[str] | None = None,
82
+ scope: str = "global",
83
+ sensitivity: str = "internal",
84
+ generate_embeddings: bool = True,
85
+ extract_entities: bool = True,
86
+ ) -> IngestResult:
87
+ """Ingest a batch of records into the context store.
88
+
89
+ Parameters
90
+ ----------
91
+ records : list[dict]
92
+ Records to ingest. Each must have a "content" key.
93
+ source : str
94
+ Source identifier (e.g., "airflow-pipeline", "webhook", "manual").
95
+ tags : list[str], optional
96
+ Tags to apply to all records.
97
+ scope : str
98
+ Visibility scope (default: "global").
99
+ sensitivity : str
100
+ Sensitivity level (default: "internal").
101
+ generate_embeddings : bool
102
+ Whether to generate embeddings (platform mode only).
103
+ extract_entities : bool
104
+ Whether to extract entities (platform mode only).
105
+
106
+ Returns
107
+ -------
108
+ IngestResult
109
+ Summary of the ingestion.
110
+ """
111
+ parsed = [IngestRecord(**r) if isinstance(r, dict) else r for r in records]
112
+ result = self._backend.ingest(
113
+ records=parsed,
114
+ source=source,
115
+ tags=tags or [],
116
+ scope=scope,
117
+ sensitivity=sensitivity,
118
+ generate_embeddings=generate_embeddings,
119
+ extract_entities=extract_entities,
120
+ )
121
+
122
+ # Fire event handlers
123
+ self._fire_event("context.created", {
124
+ "source": source,
125
+ "entries_created": result.entries_created,
126
+ "entry_ids": result.entry_ids,
127
+ })
128
+
129
+ _report_ingest(source, result.entries_created)
130
+ return result
131
+
132
+ def search(
133
+ self,
134
+ query: str,
135
+ top_k: int = 20,
136
+ mode: str = "hybrid",
137
+ filters: dict[str, Any] | None = None,
138
+ ) -> SearchResult:
139
+ """Search the context store.
140
+
141
+ Parameters
142
+ ----------
143
+ query : str
144
+ Search query text.
145
+ top_k : int
146
+ Maximum results to return.
147
+ mode : str
148
+ Search mode: "semantic", "keyword", or "hybrid".
149
+ filters : dict, optional
150
+ Filters: tags, sources, scopes, sensitivity, date_from, date_to.
151
+
152
+ Returns
153
+ -------
154
+ SearchResult
155
+ Search results with scores and highlights.
156
+ """
157
+ result = self._backend.search(
158
+ query=query,
159
+ top_k=top_k,
160
+ mode=mode,
161
+ filters=filters or {},
162
+ )
163
+ _report_search(query, mode, result.total)
164
+ return result
165
+
166
+ def on(
167
+ self,
168
+ event_type: str,
169
+ callback: Callable[..., Any],
170
+ filter: dict[str, Any] | None = None,
171
+ ) -> str:
172
+ """Subscribe to context events.
173
+
174
+ Parameters
175
+ ----------
176
+ event_type : str
177
+ Event type to listen for (e.g., "context.created").
178
+ callback : callable
179
+ Function to call when event occurs.
180
+ filter : dict, optional
181
+ Filter criteria (e.g., {"tags": ["support"]}).
182
+
183
+ Returns
184
+ -------
185
+ str
186
+ Subscription ID.
187
+ """
188
+ import uuid
189
+ sub_id = str(uuid.uuid4())[:8]
190
+ if event_type not in self._event_handlers:
191
+ self._event_handlers[event_type] = []
192
+ self._event_handlers[event_type].append({
193
+ "id": sub_id,
194
+ "callback": callback,
195
+ "filter": filter or {},
196
+ })
197
+ return sub_id
198
+
199
+ def off(self, event_type: str, subscription_id: str) -> None:
200
+ """Remove an event subscription."""
201
+ if event_type in self._event_handlers:
202
+ self._event_handlers[event_type] = [
203
+ h for h in self._event_handlers[event_type]
204
+ if h["id"] != subscription_id
205
+ ]
206
+
207
+ def _fire_event(self, event_type: str, data: dict[str, Any]) -> None:
208
+ """Fire event handlers matching the event type and filters."""
209
+ for handler in self._event_handlers.get(event_type, []):
210
+ filter_spec = handler.get("filter", {})
211
+ # Check tag filter
212
+ if "tags" in filter_spec and "tags" in data:
213
+ if not any(t in data.get("tags", []) for t in filter_spec["tags"]):
214
+ continue
215
+ try:
216
+ handler["callback"](data)
217
+ except Exception:
218
+ logger.exception("Error in event handler for %s", event_type)
219
+
220
+
221
+ # ── Telemetry helpers ──────────────────────────────────────────
222
+
223
+ def _report_init(backend_type: str) -> None:
224
+ try:
225
+ from rotalabs._telemetry import is_enabled, report_event
226
+ if is_enabled():
227
+ report_event("context.engine.init", {"backend": backend_type})
228
+ except ImportError:
229
+ pass
230
+
231
+
232
+ def _report_ingest(source: str, count: int) -> None:
233
+ try:
234
+ from rotalabs._telemetry import is_enabled, report_event
235
+ if is_enabled():
236
+ report_event("context.ingest", {"source": source, "count": count})
237
+ except ImportError:
238
+ pass
239
+
240
+
241
+ def _report_search(query: str, mode: str, total: int) -> None:
242
+ try:
243
+ from rotalabs._telemetry import is_enabled, report_event
244
+ if is_enabled():
245
+ report_event("context.search", {"mode": mode, "total": total})
246
+ except ImportError:
247
+ pass
@@ -0,0 +1,99 @@
1
+ """Data models for rotalabs-context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class Scope(str, enum.Enum):
13
+ """Visibility scope for context entries."""
14
+ GLOBAL = "global"
15
+ TEAM = "team"
16
+ AGENT = "agent"
17
+ PROJECT = "project"
18
+ USER = "user"
19
+
20
+
21
+ class Sensitivity(str, enum.Enum):
22
+ """Sensitivity classification for context entries."""
23
+ PUBLIC = "public"
24
+ INTERNAL = "internal"
25
+ CONFIDENTIAL = "confidential"
26
+ RESTRICTED = "restricted"
27
+
28
+
29
+ class ContextEntry(BaseModel):
30
+ """A single context entry in the store."""
31
+ id: str
32
+ source: str
33
+ source_id: str | None = None
34
+ content_type: str = "text"
35
+ title: str | None = None
36
+ content: str
37
+ content_hash: str
38
+ tags: list[str] = Field(default_factory=list)
39
+ scope: str = "global"
40
+ sensitivity: str = "internal"
41
+ metadata: dict[str, Any] = Field(default_factory=dict)
42
+ created_at: datetime | None = None
43
+
44
+
45
+ class SearchHit(BaseModel):
46
+ """A single search result."""
47
+ entry: ContextEntry
48
+ score: float
49
+ highlights: list[str] = Field(default_factory=list)
50
+ match_type: str = "hybrid"
51
+
52
+
53
+ class SearchResult(BaseModel):
54
+ """Search response containing hits."""
55
+ query: str
56
+ mode: str
57
+ results: list[SearchHit]
58
+ total: int
59
+ took_ms: float = 0.0
60
+
61
+
62
+ class IngestResult(BaseModel):
63
+ """Result of a batch ingest operation."""
64
+ entries_created: int
65
+ embeddings_generated: int
66
+ entities_extracted: int
67
+ entry_ids: list[str] = Field(default_factory=list)
68
+
69
+
70
+ class IngestRecord(BaseModel):
71
+ """A single record to ingest."""
72
+ content: str
73
+ title: str | None = None
74
+ content_type: str = "text"
75
+ source_id: str | None = None
76
+ metadata: dict[str, Any] = Field(default_factory=dict)
77
+
78
+
79
+ class Subscription(BaseModel):
80
+ """A context subscription."""
81
+ id: str
82
+ subscriber_id: str
83
+ subscriber_type: str
84
+ filter_tags: list[str] = Field(default_factory=list)
85
+ filter_scopes: list[str] = Field(default_factory=list)
86
+ filter_sources: list[str] = Field(default_factory=list)
87
+ delivery_method: str
88
+ webhook_url: str | None = None
89
+ is_active: bool = True
90
+
91
+
92
+ class Entity(BaseModel):
93
+ """An extracted entity."""
94
+ id: str
95
+ name: str
96
+ entity_type: str
97
+ source_entry_id: str | None = None
98
+ metadata: dict[str, Any] = Field(default_factory=dict)
99
+ mentions_count: int = 1
@@ -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,15 @@
1
+ rotalabs_context/__init__.py,sha256=NyCfzb7QDx6WCsLSk6tPqWqzHy4D9wi_t6fk61lMDKQ,813
2
+ rotalabs_context/_telemetry.py,sha256=_orcKtb2a0Wz923O_68_ctggcStRufaE-UBrlPD4bXk,879
3
+ rotalabs_context/_version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
4
+ rotalabs_context/engine.py,sha256=kl_esUPUrtjkqBzt8suHkK7066c8wyk7IzH2J6DNJtg,7691
5
+ rotalabs_context/models.py,sha256=5xkkZVPiwOc6ol4AfMY7kOEKogzKA2sMSxegyv7gyjE,2460
6
+ rotalabs_context/adapters/__init__.py,sha256=ufd8HaKQAk03sxkPIMs-6YOXCOpsZ00bxDObdW627Qs,77
7
+ rotalabs_context/adapters/airflow.py,sha256=vkeiVBCxjPeSEgJtOPBC_tE35laXAw0xZpxCtNrSLHk,2451
8
+ rotalabs_context/backends/__init__.py,sha256=mFsVHMQSSYaXuUfmB_0kJw9iS99QiMJV-afdZFj8aR8,55
9
+ rotalabs_context/backends/base.py,sha256=Uo7VM1zXi1d8wk2bI8wOjXFBgf2n0ANxWJhK5a68-KE,703
10
+ rotalabs_context/backends/memory.py,sha256=jP7IBXa_u-4sFfiJe3B15VHDqPaKRKGAurj30kn4u3A,4072
11
+ rotalabs_context/backends/platform.py,sha256=7R0Y_3sP-yrMNKIUwHD6a4bnYmWKzV0huvaBb7IxL78,2835
12
+ rotalabs_context-1.0.0.dist-info/METADATA,sha256=zfwpu_3y9oFOWdy6WrrD6XBAyzlk9DXLiRZ22wn6rPg,4110
13
+ rotalabs_context-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ rotalabs_context-1.0.0.dist-info/licenses/LICENSE,sha256=NHBABKRQussR78sGiLyZiA98rt-Q2SAvaMwSwFkctGg,105
15
+ rotalabs_context-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.