darkplex-core/cortex/intelligence/temporal.py
Claudia fd7d75c0ed
Some checks failed
Tests / test (push) Failing after 2s
Merge darkplex-core into cortex — unified intelligence layer v0.2.0
- 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
2026-02-12 08:43:02 +01:00

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()