Some checks failed
Tests / test (push) Failing after 2s
- Merged all unique darkplex-core modules into cortex: - intelligence/ subfolder (anticipator, collective, shared_memory, knowledge_cleanup, temporal, llm_extractor, loop) - governance/ subfolder (policy engine, risk scorer, evidence, enforcer, report generator) - entity_manager.py, knowledge_extractor.py - Fixed bare 'from intelligence.' imports to 'from cortex.intelligence.' - Added 'darkplex' CLI alias alongside 'cortex' - Package renamed to darkplex-core v0.2.0 - 405 tests passing (was 234) - 14 new test files covering all merged modules
193 lines
6.5 KiB
Python
193 lines
6.5 KiB
Python
"""Temporal Context API: chronological knowledge retrieval.
|
|
|
|
Queries NATS events and ChromaDB with a time dimension to answer:
|
|
"What do we know about X, chronologically?"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default config from environment
|
|
NATS_URL = os.environ.get("NATS_URL", "nats://localhost:4222")
|
|
CHROMADB_URL = os.environ.get("CHROMADB_URL", "http://localhost:8000")
|
|
|
|
|
|
@dataclass
|
|
class TemporalEntry:
|
|
"""A knowledge entry with temporal metadata."""
|
|
|
|
timestamp: datetime
|
|
source: str # "nats" or "chromadb"
|
|
topic: str
|
|
content: str
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
relevance_score: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class TemporalQuery:
|
|
"""Query parameters for temporal context retrieval."""
|
|
|
|
topic: str
|
|
start_time: datetime | None = None
|
|
end_time: datetime | None = None
|
|
limit: int = 50
|
|
sources: list[str] = field(default_factory=lambda: ["nats", "chromadb"])
|
|
|
|
|
|
class TemporalContext:
|
|
"""Retrieves chronological knowledge from NATS events and ChromaDB.
|
|
|
|
Usage:
|
|
ctx = TemporalContext()
|
|
entries = await ctx.query(TemporalQuery(topic="ssl-cert"))
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
nats_url: str | None = None,
|
|
chromadb_url: str | None = None,
|
|
) -> None:
|
|
self.nats_url = nats_url or NATS_URL
|
|
self.chromadb_url = chromadb_url or CHROMADB_URL
|
|
self._nats_client: Any = None
|
|
self._chroma_client: Any = None
|
|
|
|
async def connect(self) -> None:
|
|
"""Initialize connections to NATS and ChromaDB."""
|
|
try:
|
|
import nats
|
|
self._nats_client = await nats.connect(self.nats_url)
|
|
logger.info("Connected to NATS: %s", self.nats_url)
|
|
except Exception:
|
|
logger.exception("Failed to connect to NATS")
|
|
|
|
try:
|
|
import chromadb
|
|
self._chroma_client = chromadb.HttpClient(host=self.chromadb_url)
|
|
logger.info("Connected to ChromaDB: %s", self.chromadb_url)
|
|
except Exception:
|
|
logger.exception("Failed to connect to ChromaDB")
|
|
|
|
async def query(self, query: TemporalQuery) -> list[TemporalEntry]:
|
|
"""Query temporal context across configured sources.
|
|
|
|
Returns entries sorted chronologically (oldest first).
|
|
"""
|
|
entries: list[TemporalEntry] = []
|
|
|
|
if "nats" in query.sources and self._nats_client:
|
|
nats_entries = await self._query_nats(query)
|
|
entries.extend(nats_entries)
|
|
|
|
if "chromadb" in query.sources and self._chroma_client:
|
|
chroma_entries = self._query_chromadb(query)
|
|
entries.extend(chroma_entries)
|
|
|
|
# Sort chronologically
|
|
entries.sort(key=lambda e: e.timestamp)
|
|
|
|
# Apply limit
|
|
if query.limit:
|
|
entries = entries[:query.limit]
|
|
|
|
return entries
|
|
|
|
async def _query_nats(self, query: TemporalQuery) -> list[TemporalEntry]:
|
|
"""Query NATS JetStream for historical events matching the topic."""
|
|
entries: list[TemporalEntry] = []
|
|
try:
|
|
js = self._nats_client.jetstream()
|
|
subject = f"darkplex.*.{query.topic}.>"
|
|
|
|
# Get messages from the stream
|
|
sub = await js.subscribe(subject, ordered_consumer=True)
|
|
count = 0
|
|
async for msg in sub.messages:
|
|
if count >= query.limit:
|
|
break
|
|
|
|
timestamp = datetime.fromtimestamp(
|
|
msg.headers.get("Nats-Time-Stamp", 0) if msg.headers else 0,
|
|
tz=timezone.utc,
|
|
)
|
|
|
|
if query.start_time and timestamp < query.start_time:
|
|
continue
|
|
if query.end_time and timestamp > query.end_time:
|
|
continue
|
|
|
|
entries.append(TemporalEntry(
|
|
timestamp=timestamp,
|
|
source="nats",
|
|
topic=query.topic,
|
|
content=msg.data.decode() if msg.data else "",
|
|
metadata={"subject": msg.subject},
|
|
))
|
|
count += 1
|
|
|
|
except Exception:
|
|
logger.exception("NATS temporal query failed for topic: %s", query.topic)
|
|
|
|
return entries
|
|
|
|
def _query_chromadb(self, query: TemporalQuery) -> list[TemporalEntry]:
|
|
"""Query ChromaDB for semantically relevant entries with time filtering."""
|
|
entries: list[TemporalEntry] = []
|
|
try:
|
|
collection = self._chroma_client.get_or_create_collection("darkplex_knowledge")
|
|
|
|
where_filter: dict[str, Any] = {}
|
|
if query.start_time:
|
|
where_filter["timestamp"] = {"$gte": query.start_time.isoformat()}
|
|
if query.end_time:
|
|
if "timestamp" in where_filter:
|
|
where_filter = {
|
|
"$and": [
|
|
{"timestamp": {"$gte": query.start_time.isoformat()}},
|
|
{"timestamp": {"$lte": query.end_time.isoformat()}},
|
|
]
|
|
}
|
|
else:
|
|
where_filter["timestamp"] = {"$lte": query.end_time.isoformat()}
|
|
|
|
results = collection.query(
|
|
query_texts=[query.topic],
|
|
n_results=query.limit,
|
|
where=where_filter if where_filter else None,
|
|
)
|
|
|
|
if results and results.get("documents"):
|
|
for i, doc in enumerate(results["documents"][0]):
|
|
meta = results["metadatas"][0][i] if results.get("metadatas") else {}
|
|
ts_str = meta.get("timestamp", "")
|
|
try:
|
|
ts = datetime.fromisoformat(ts_str)
|
|
except (ValueError, TypeError):
|
|
ts = datetime.now(timezone.utc)
|
|
|
|
entries.append(TemporalEntry(
|
|
timestamp=ts,
|
|
source="chromadb",
|
|
topic=query.topic,
|
|
content=doc,
|
|
metadata=meta,
|
|
relevance_score=results["distances"][0][i] if results.get("distances") else 0.0,
|
|
))
|
|
|
|
except Exception:
|
|
logger.exception("ChromaDB temporal query failed for topic: %s", query.topic)
|
|
|
|
return entries
|
|
|
|
async def close(self) -> None:
|
|
"""Close connections."""
|
|
if self._nats_client:
|
|
await self._nats_client.close()
|