Initial commit: add .gitignore and README
Some checks failed
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled

This commit is contained in:
defiQUG
2026-02-09 21:51:42 -08:00
commit c052b07662
3146 changed files with 808305 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
"""Memory system: working, episodic, reflective, semantic, procedural, trust, consolidation."""
from fusionagi.memory.working import WorkingMemory
from fusionagi.memory.episodic import EpisodicMemory
from fusionagi.memory.reflective import ReflectiveMemory
from fusionagi.memory.semantic import SemanticMemory
from fusionagi.memory.procedural import ProceduralMemory
from fusionagi.memory.trust import TrustMemory
from fusionagi.memory.consolidation import ConsolidationJob
from fusionagi.memory.service import MemoryService, VectorMemory
from fusionagi.memory.vector_pgvector import create_vector_memory_pgvector, VectorMemoryPgvector
from fusionagi.memory.postgres_backend import (
MemoryBackend,
InMemoryBackend,
create_postgres_backend,
)
from fusionagi.memory.semantic_graph import SemanticGraphMemory
from fusionagi.memory.sharding import Shard, shard_context
from fusionagi.memory.scratchpad import LatentScratchpad, ThoughtState
__all__ = [
"WorkingMemory",
"EpisodicMemory",
"ReflectiveMemory",
"SemanticMemory",
"ProceduralMemory",
"TrustMemory",
"ConsolidationJob",
"MemoryService",
"VectorMemory",
"create_vector_memory_pgvector",
"VectorMemoryPgvector",
"MemoryBackend",
"InMemoryBackend",
"create_postgres_backend",
"SemanticGraphMemory",
"Shard",
"shard_context",
"LatentScratchpad",
"ThoughtState",
"ThoughtVersioning",
"ThoughtStateSnapshot",
]

View File

@@ -0,0 +1,87 @@
"""Consolidation: distillation of experiences into knowledge; write/forget rules for AGI."""
from typing import Any, Callable, Protocol
from fusionagi._logger import logger
class EpisodicLike(Protocol):
def get_lessons(self, limit: int) -> list[dict[str, Any]]: ...
def get_recent(self, limit: int) -> list[dict[str, Any]]: ...
class ReflectiveLike(Protocol):
def get_lessons(self, limit: int) -> list[dict[str, Any]]: ...
class SemanticLike(Protocol):
def add_fact(self, fact_id: str, statement: str, source: str, domain: str, metadata: dict | None) -> None: ...
class ConsolidationJob:
"""
Periodic distillation: take recent episodic/reflective lessons and
write summarized facts into semantic memory. Write/forget rules
are applied by the distiller callback.
"""
def __init__(
self,
episodic: EpisodicLike | None = None,
reflective: ReflectiveLike | None = None,
semantic: SemanticLike | None = None,
distiller: Callable[[list[dict[str, Any]]], list[dict[str, Any]]] | None = None,
) -> None:
self._episodic = episodic
self._reflective = reflective
self._semantic = semantic
self._distiller = distiller or _default_distiller
def run(self, episodic_limit: int = 100, reflective_limit: int = 50) -> int:
"""
Run consolidation: gather recent lessons, distill, write to semantic.
Returns number of facts written.
"""
lessons: list[dict[str, Any]] = []
if self._episodic:
try:
lessons.extend(self._episodic.get_recent(episodic_limit) if hasattr(self._episodic, "get_recent") else [])
except Exception:
pass
if self._reflective:
try:
lessons.extend(self._reflective.get_lessons(reflective_limit))
except Exception:
pass
if not lessons:
return 0
facts = self._distiller(lessons)
written = 0
if self._semantic and facts:
for i, f in enumerate(facts[:50]):
fact_id = f.get("fact_id", f"consolidated_{i}")
statement = f.get("statement", str(f))
source = f.get("source", "consolidation")
domain = f.get("domain", "general")
try:
self._semantic.add_fact(fact_id, statement, source=source, domain=domain, metadata=f)
written += 1
except Exception:
logger.exception("Consolidation: failed to add fact", extra={"fact_id": fact_id})
logger.info("Consolidation run", extra={"lessons": len(lessons), "facts_written": written})
return written
def _default_distiller(lessons: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Default: turn each lesson into one fact (summary)."""
out = []
for i, le in enumerate(lessons[-100:]):
outcome = le.get("outcome", le.get("result", ""))
task_id = le.get("task_id", "")
out.append({
"fact_id": f"cons_{task_id}_{i}",
"statement": f"Task {task_id} outcome: {outcome}",
"source": "consolidation",
"domain": "general",
})
return out

View File

@@ -0,0 +1,226 @@
"""Episodic memory: append-only log of task/step outcomes; query by task_id or time range.
Episodic memory stores historical records of agent actions and outcomes:
- Task execution traces
- Step outcomes (success/failure)
- Tool invocation results
- Decision points and their outcomes
"""
import time
from typing import Any, Callable, Iterator
from fusionagi._logger import logger
from fusionagi._time import utc_now_iso
class EpisodicMemory:
"""
Append-only log of task and step outcomes.
Features:
- Time-stamped event logging
- Query by task ID
- Query by time range
- Query by event type
- Statistical summaries
- Memory size limits with optional archival
"""
def __init__(self, max_entries: int = 10000) -> None:
"""
Initialize episodic memory.
Args:
max_entries: Maximum entries before oldest are archived/removed.
"""
self._entries: list[dict[str, Any]] = []
self._by_task: dict[str, list[int]] = {} # task_id -> indices into _entries
self._by_type: dict[str, list[int]] = {} # event_type -> indices
self._max_entries = max_entries
self._archived_count = 0
def append(
self,
task_id: str,
event: dict[str, Any],
event_type: str | None = None,
) -> int:
"""
Append an episodic entry.
Args:
task_id: Task identifier this event belongs to.
event: Event data dictionary.
event_type: Optional event type for categorization (e.g., "step_done", "tool_call").
Returns:
Index of the appended entry.
"""
# Enforce size limits
if len(self._entries) >= self._max_entries:
self._archive_oldest(self._max_entries // 10)
# Add metadata
entry = {
**event,
"task_id": task_id,
"timestamp": event.get("timestamp", time.monotonic()),
"datetime": event.get("datetime", utc_now_iso()),
}
if event_type:
entry["event_type"] = event_type
idx = len(self._entries)
self._entries.append(entry)
# Index by task
self._by_task.setdefault(task_id, []).append(idx)
# Index by type if provided
etype = event_type or event.get("type") or event.get("event_type")
if etype:
self._by_type.setdefault(etype, []).append(idx)
return idx
def get_by_task(self, task_id: str, limit: int | None = None) -> list[dict[str, Any]]:
"""Return all entries for a task (copy), optionally limited."""
indices = self._by_task.get(task_id, [])
if limit:
indices = indices[-limit:]
return [self._entries[i].copy() for i in indices]
def get_by_type(self, event_type: str, limit: int | None = None) -> list[dict[str, Any]]:
"""Return entries of a specific type."""
indices = self._by_type.get(event_type, [])
if limit:
indices = indices[-limit:]
return [self._entries[i].copy() for i in indices]
def get_recent(self, limit: int = 100) -> list[dict[str, Any]]:
"""Return most recent entries (copy)."""
return [e.copy() for e in self._entries[-limit:]]
def get_by_time_range(
self,
start_timestamp: float | None = None,
end_timestamp: float | None = None,
limit: int | None = None,
) -> list[dict[str, Any]]:
"""
Return entries within a time range (using monotonic timestamps).
Args:
start_timestamp: Start of range (inclusive).
end_timestamp: End of range (inclusive).
limit: Maximum entries to return.
"""
results = []
for entry in self._entries:
ts = entry.get("timestamp", 0)
if start_timestamp and ts < start_timestamp:
continue
if end_timestamp and ts > end_timestamp:
continue
results.append(entry.copy())
if limit and len(results) >= limit:
break
return results
def query(
self,
filter_fn: Callable[[dict[str, Any]], bool],
limit: int | None = None,
) -> list[dict[str, Any]]:
"""
Query entries using a custom filter function.
Args:
filter_fn: Function that returns True for entries to include.
limit: Maximum entries to return.
"""
results = []
for entry in self._entries:
if filter_fn(entry):
results.append(entry.copy())
if limit and len(results) >= limit:
break
return results
def get_task_summary(self, task_id: str) -> dict[str, Any]:
"""
Get a summary of episodes for a task.
Returns statistics like count, first/last timestamps, event types.
"""
entries = self.get_by_task(task_id)
if not entries:
return {"task_id": task_id, "count": 0}
event_types: dict[str, int] = {}
success_count = 0
failure_count = 0
for entry in entries:
etype = entry.get("event_type") or entry.get("type") or "unknown"
event_types[etype] = event_types.get(etype, 0) + 1
if entry.get("success"):
success_count += 1
elif entry.get("error") or entry.get("success") is False:
failure_count += 1
return {
"task_id": task_id,
"count": len(entries),
"first_timestamp": entries[0].get("datetime"),
"last_timestamp": entries[-1].get("datetime"),
"event_types": event_types,
"success_count": success_count,
"failure_count": failure_count,
}
def get_statistics(self) -> dict[str, Any]:
"""Get overall memory statistics."""
return {
"total_entries": len(self._entries),
"archived_entries": self._archived_count,
"task_count": len(self._by_task),
"event_type_count": len(self._by_type),
"event_types": list(self._by_type.keys()),
}
def _archive_oldest(self, count: int) -> None:
"""Archive/remove oldest entries to enforce size limits."""
if count <= 0 or count >= len(self._entries):
return
logger.info(
"Archiving episodic memory entries",
extra={"count": count, "total": len(self._entries)},
)
# Remove oldest entries
self._entries = self._entries[count:]
self._archived_count += count
# Rebuild indices (entries shifted)
self._by_task = {}
self._by_type = {}
for idx, entry in enumerate(self._entries):
task_id = entry.get("task_id")
if task_id:
self._by_task.setdefault(task_id, []).append(idx)
etype = entry.get("event_type") or entry.get("type")
if etype:
self._by_type.setdefault(etype, []).append(idx)
def clear(self) -> None:
"""Clear all entries (for tests)."""
self._entries.clear()
self._by_task.clear()
self._by_type.clear()
self._archived_count = 0

View File

@@ -0,0 +1,231 @@
"""Optional Postgres backend for memory. Requires: pip install fusionagi[memory]."""
from abc import ABC, abstractmethod
from typing import Any
from fusionagi._logger import logger
class MemoryBackend(ABC):
"""Abstract backend for persistent memory storage."""
@abstractmethod
def store(
self,
id: str,
tenant_id: str,
user_id: str,
session_id: str,
type: str,
content: dict[str, Any],
metadata: dict[str, Any] | None = None,
retention_policy: str = "session",
) -> None:
"""Store a memory item."""
...
@abstractmethod
def get(self, id: str) -> dict[str, Any] | None:
"""Get a memory item by id."""
...
@abstractmethod
def query(
self,
tenant_id: str,
user_id: str | None = None,
session_id: str | None = None,
type: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
"""Query memory items."""
...
class InMemoryBackend(MemoryBackend):
"""In-memory implementation for development."""
def __init__(self) -> None:
self._store: dict[str, dict[str, Any]] = {}
def store(
self,
id: str,
tenant_id: str,
user_id: str,
session_id: str,
type: str,
content: dict[str, Any],
metadata: dict[str, Any] | None = None,
retention_policy: str = "session",
) -> None:
self._store[id] = {
"id": id,
"tenant_id": tenant_id,
"user_id": user_id,
"session_id": session_id,
"type": type,
"content": content,
"metadata": metadata or {},
"retention_policy": retention_policy,
}
def get(self, id: str) -> dict[str, Any] | None:
return self._store.get(id)
def query(
self,
tenant_id: str,
user_id: str | None = None,
session_id: str | None = None,
type: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
out = []
for v in self._store.values():
if v["tenant_id"] != tenant_id:
continue
if user_id and v["user_id"] != user_id:
continue
if session_id and v["session_id"] != session_id:
continue
if type and v["type"] != type:
continue
out.append(v)
if len(out) >= limit:
break
return out
def create_postgres_backend(connection_string: str) -> MemoryBackend | None:
"""Create Postgres-backed MemoryBackend when psycopg is available."""
try:
import psycopg
except ImportError:
logger.debug("psycopg not installed; use pip install fusionagi[memory]")
return None
return PostgresMemoryBackend(connection_string)
class PostgresMemoryBackend(MemoryBackend):
"""Postgres-backed memory storage."""
def __init__(self, connection_string: str) -> None:
self._conn_str = connection_string
self._init_schema()
def _init_schema(self) -> None:
import psycopg
with psycopg.connect(self._conn_str) as conn:
with conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS memory_items (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
user_id TEXT NOT NULL,
session_id TEXT NOT NULL,
type TEXT NOT NULL,
content JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
retention_policy TEXT DEFAULT 'session',
created_at TIMESTAMPTZ DEFAULT NOW()
)
"""
)
conn.commit()
def store(
self,
id: str,
tenant_id: str,
user_id: str,
session_id: str,
type: str,
content: dict[str, Any],
metadata: dict[str, Any] | None = None,
retention_policy: str = "session",
) -> None:
import json
import psycopg
with psycopg.connect(self._conn_str) as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO memory_items (id, tenant_id, user_id, session_id, type, content, metadata, retention_policy)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, metadata = EXCLUDED.metadata
""",
(id, tenant_id, user_id, session_id, type, json.dumps(content), json.dumps(metadata or {}), retention_policy),
)
conn.commit()
def get(self, id: str) -> dict[str, Any] | None:
import json
import psycopg
with psycopg.connect(self._conn_str) as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id, tenant_id, user_id, session_id, type, content, metadata, retention_policy FROM memory_items WHERE id = %s",
(id,),
)
row = cur.fetchone()
if not row:
return None
return {
"id": row[0],
"tenant_id": row[1],
"user_id": row[2],
"session_id": row[3],
"type": row[4],
"content": json.loads(row[5]) if row[5] else {},
"metadata": json.loads(row[6]) if row[6] else {},
"retention_policy": row[7],
}
def query(
self,
tenant_id: str,
user_id: str | None = None,
session_id: str | None = None,
type: str | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
import json
import psycopg
q = "SELECT id, tenant_id, user_id, session_id, type, content, metadata, retention_policy FROM memory_items WHERE tenant_id = %s"
params: list[Any] = [tenant_id]
if user_id:
q += " AND user_id = %s"
params.append(user_id)
if session_id:
q += " AND session_id = %s"
params.append(session_id)
if type:
q += " AND type = %s"
params.append(type)
q += " ORDER BY created_at DESC LIMIT %s"
params.append(limit)
with psycopg.connect(self._conn_str) as conn:
with conn.cursor() as cur:
cur.execute(q, params)
rows = cur.fetchall()
return [
{
"id": r[0],
"tenant_id": r[1],
"user_id": r[2],
"session_id": r[3],
"type": r[4],
"content": json.loads(r[5]) if r[5] else {},
"metadata": json.loads(r[6]) if r[6] else {},
"retention_policy": r[7],
}
for r in rows
]

View File

@@ -0,0 +1,55 @@
"""Procedural memory: reusable skills/workflows for AGI."""
from typing import Any
from fusionagi.schemas.skill import Skill
from fusionagi._logger import logger
class ProceduralMemory:
"""
Skill store: reusable workflows and procedures. Invokable by name;
write/update rules enforced by caller.
"""
def __init__(self, max_skills: int = 5000) -> None:
self._skills: dict[str, Skill] = {}
self._by_name: dict[str, str] = {} # name -> skill_id (latest)
self._max_skills = max_skills
def add_skill(self, skill: Skill) -> None:
"""Register a skill (overwrites same skill_id)."""
if len(self._skills) >= self._max_skills and skill.skill_id not in self._skills:
self._evict_one()
self._skills[skill.skill_id] = skill
self._by_name[skill.name] = skill.skill_id
logger.debug("Procedural memory: skill added", extra={"skill_id": skill.skill_id, "name": skill.name})
def get_skill(self, skill_id: str) -> Skill | None:
"""Return skill by id or None."""
return self._skills.get(skill_id)
def get_skill_by_name(self, name: str) -> Skill | None:
"""Return latest skill with this name or None."""
sid = self._by_name.get(name)
return self._skills.get(sid) if sid else None
def list_skills(self, limit: int = 200) -> list[Skill]:
"""Return skills (e.g. for planner)."""
return list(self._skills.values())[-limit:]
def remove_skill(self, skill_id: str) -> bool:
"""Remove skill. Returns True if existed."""
if skill_id not in self._skills:
return False
name = self._skills[skill_id].name
del self._skills[skill_id]
if self._by_name.get(name) == skill_id:
del self._by_name[name]
return True
def _evict_one(self) -> None:
if not self._skills:
return
rid = next(iter(self._skills))
self.remove_skill(rid)

View File

@@ -0,0 +1,31 @@
"""Reflective memory: simple store for lessons learned / heuristics (used by Phase 3)."""
from typing import Any
class ReflectiveMemory:
"""Simple store for lessons and heuristics; append-only list or key-value."""
def __init__(self) -> None:
self._lessons: list[dict[str, Any]] = []
self._heuristics: dict[str, Any] = {}
def add_lesson(self, lesson: dict[str, Any]) -> None:
"""Append a lesson (e.g. from Critic)."""
self._lessons.append(lesson)
def get_lessons(self, limit: int = 50) -> list[dict[str, Any]]:
"""Return recent lessons (copy)."""
return [l.copy() for l in self._lessons[-limit:]]
def set_heuristic(self, key: str, value: Any) -> None:
"""Set a heuristic (e.g. strategy hint)."""
self._heuristics[key] = value
def get_heuristic(self, key: str) -> Any:
"""Get heuristic by key."""
return self._heuristics.get(key)
def get_all_heuristics(self) -> dict[str, Any]:
"""Return all heuristics (copy)."""
return dict(self._heuristics)

View File

@@ -0,0 +1,70 @@
"""Latent scratchpad: internal reasoning buffers for hypotheses and discarded paths."""
from __future__ import annotations
from collections import deque
from dataclasses import dataclass, field
from typing import Any
from fusionagi._logger import logger
@dataclass
class ThoughtState:
"""Internal reasoning state: hypotheses, partial conclusions, discarded paths."""
hypotheses: list[str] = field(default_factory=list)
partial_conclusions: list[str] = field(default_factory=list)
discarded_paths: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
class LatentScratchpad:
"""
Internal buffer for intermediate reasoning; not exposed to user.
Stores hypotheses, discarded paths, partial conclusions for meta-tracking.
"""
def __init__(self, max_hypotheses: int = 100, max_discarded: int = 50) -> None:
self._hypotheses: deque[str] = deque(maxlen=max_hypotheses)
self._partial: deque[str] = deque(maxlen=50)
self._discarded: deque[str] = deque(maxlen=max_discarded)
self._metadata: dict[str, Any] = {}
def append_hypothesis(self, hypothesis: str) -> None:
"""Append a reasoning hypothesis."""
self._hypotheses.append(hypothesis)
logger.debug("Scratchpad: hypothesis appended", extra={"len": len(self._hypotheses)})
def append_discarded(self, path: str) -> None:
"""Append a discarded reasoning path."""
self._discarded.append(path)
def append_partial(self, conclusion: str) -> None:
"""Append a partial conclusion."""
self._partial.append(conclusion)
def get_intermediate(self) -> ThoughtState:
"""Get current intermediate state."""
return ThoughtState(
hypotheses=list(self._hypotheses),
partial_conclusions=list(self._partial),
discarded_paths=list(self._discarded),
metadata=dict(self._metadata),
)
def clear(self) -> None:
"""Clear scratchpad."""
self._hypotheses.clear()
self._partial.clear()
self._discarded.clear()
self._metadata.clear()
def set_metadata(self, key: str, value: Any) -> None:
"""Set metadata entry."""
self._metadata[key] = value
def get_metadata(self, key: str, default: Any = None) -> Any:
"""Get metadata entry."""
return self._metadata.get(key, default)

View File

@@ -0,0 +1,55 @@
"""Semantic memory: facts, policies, domain knowledge for AGI."""
from typing import Any
from fusionagi._logger import logger
class SemanticMemory:
"""Stores facts, policies, domain knowledge. Queryable and updatable."""
def __init__(self, max_facts: int = 50000) -> None:
self._facts: dict[str, dict[str, Any]] = {}
self._by_key: dict[str, list[str]] = {}
self._max_facts = max_facts
def add_fact(self, fact_id: str, statement: str, source: str = "", domain: str = "", metadata: dict[str, Any] | None = None) -> None:
if len(self._facts) >= self._max_facts and fact_id not in self._facts:
self._evict_one()
entry = {"statement": statement, "source": source, "domain": domain, "metadata": metadata or {}}
self._facts[fact_id] = entry
if domain:
self._by_key.setdefault(domain, []).append(fact_id)
logger.debug("Semantic memory: fact added", extra={"fact_id": fact_id, "domain": domain})
def get_fact(self, fact_id: str) -> dict[str, Any] | None:
return self._facts.get(fact_id)
def query(self, domain: str | None = None, limit: int = 100) -> list[dict[str, Any]]:
if domain:
ids = self._by_key.get(domain, [])[-limit:]
return [self._facts[id] for id in ids if id in self._facts]
return list(self._facts.values())[-limit:]
def update_fact(self, fact_id: str, **kwargs: Any) -> bool:
if fact_id not in self._facts:
return False
for k, v in kwargs.items():
if k in self._facts[fact_id]:
self._facts[fact_id][k] = v
return True
def forget(self, fact_id: str) -> bool:
if fact_id not in self._facts:
return False
entry = self._facts.pop(fact_id)
domain = entry.get("domain", "")
if domain and fact_id in self._by_key.get(domain, []):
self._by_key[domain] = [x for x in self._by_key[domain] if x != fact_id]
return True
def _evict_one(self) -> None:
if not self._facts:
return
rid = next(iter(self._facts))
self.forget(rid)

View File

@@ -0,0 +1,106 @@
"""Semantic memory graph: nodes = AtomicSemanticUnit, edges = SemanticRelation."""
from __future__ import annotations
from collections import defaultdict
from typing import Any
from fusionagi.schemas.atomic import (
AtomicSemanticUnit,
AtomicUnitType,
SemanticRelation,
)
from fusionagi._logger import logger
class SemanticGraphMemory:
"""
Graph-backed semantic memory: nodes = atomic units, edges = relations.
Supports add_unit, add_relation, query_units, query_neighbors, query_by_type.
In-memory implementation with dict + adjacency list.
"""
def __init__(self, max_units: int = 50000) -> None:
self._units: dict[str, AtomicSemanticUnit] = {}
self._by_type: dict[AtomicUnitType, list[str]] = defaultdict(list)
self._outgoing: dict[str, list[SemanticRelation]] = defaultdict(list)
self._incoming: dict[str, list[SemanticRelation]] = defaultdict(list)
self._max_units = max_units
def add_unit(self, unit: AtomicSemanticUnit) -> None:
"""Add an atomic semantic unit."""
if len(self._units) >= self._max_units and unit.unit_id not in self._units:
self._evict_one()
self._units[unit.unit_id] = unit
self._by_type[unit.type].append(unit.unit_id)
logger.debug("Semantic graph: unit added", extra={"unit_id": unit.unit_id, "type": unit.type.value})
def add_relation(self, relation: SemanticRelation) -> None:
"""Add a relation between units."""
if relation.from_id in self._units and relation.to_id in self._units:
self._outgoing[relation.from_id].append(relation)
self._incoming[relation.to_id].append(relation)
def get_unit(self, unit_id: str) -> AtomicSemanticUnit | None:
"""Get unit by ID."""
return self._units.get(unit_id)
def query_units(
self,
unit_ids: list[str] | None = None,
unit_type: AtomicUnitType | None = None,
limit: int = 100,
) -> list[AtomicSemanticUnit]:
"""Query units by IDs or type."""
if unit_ids:
return [self._units[uid] for uid in unit_ids if uid in self._units][:limit]
if unit_type:
ids = self._by_type.get(unit_type, [])[-limit:]
return [self._units[uid] for uid in ids if uid in self._units]
return list(self._units.values())[-limit:]
def query_neighbors(
self,
unit_id: str,
direction: str = "outgoing",
relation_type: str | None = None,
) -> list[tuple[AtomicSemanticUnit, SemanticRelation]]:
"""Get neighboring units and relations."""
edges = self._outgoing[unit_id] if direction == "outgoing" else self._incoming[unit_id]
results: list[tuple[AtomicSemanticUnit, SemanticRelation]] = []
for rel in edges:
if relation_type and rel.relation_type.value != relation_type:
continue
other_id = rel.to_id if direction == "outgoing" else rel.from_id
other = self._units.get(other_id)
if other:
results.append((other, rel))
return results
def query_by_type(self, unit_type: AtomicUnitType, limit: int = 100) -> list[AtomicSemanticUnit]:
"""Query units by type."""
return self.query_units(unit_type=unit_type, limit=limit)
def ingest_decomposition(
self,
units: list[AtomicSemanticUnit],
relations: list[SemanticRelation],
) -> None:
"""Ingest a DecompositionResult into the graph."""
for u in units:
self.add_unit(u)
for r in relations:
self.add_relation(r)
def _evict_one(self) -> None:
"""Evict oldest unit (simple FIFO on first key)."""
if not self._units:
return
uid = next(iter(self._units))
unit = self._units.pop(uid, None)
if unit:
self._by_type[unit.type] = [x for x in self._by_type[unit.type] if x != uid]
self._outgoing.pop(uid, None)
self._incoming.pop(uid, None)
logger.debug("Semantic graph: evicted unit", extra={"unit_id": uid})

View File

@@ -0,0 +1,97 @@
"""Unified memory service: session, episodic, semantic, vector with tenant isolation."""
from typing import Any
from fusionagi.memory.working import WorkingMemory
from fusionagi.memory.episodic import EpisodicMemory
from fusionagi.memory.semantic import SemanticMemory
def _scoped_key(tenant_id: str, user_id: str, base: str) -> str:
"""Scope key by tenant and user."""
parts = [tenant_id or "default", user_id or "anonymous", base]
return ":".join(parts)
class VectorMemory:
"""
Vector memory for embeddings retrieval.
Stub implementation; replace with pgvector or Pinecone adapter for production.
"""
def __init__(self, max_entries: int = 10000) -> None:
self._store: list[dict[str, Any]] = []
self._max_entries = max_entries
def add(self, id: str, embedding: list[float], metadata: dict[str, Any] | None = None) -> None:
"""Add embedding (stub: stores in-memory)."""
if len(self._store) >= self._max_entries:
self._store.pop(0)
self._store.append({"id": id, "embedding": embedding, "metadata": metadata or {}})
def search(self, query_embedding: list[float], top_k: int = 10) -> list[dict[str, Any]]:
"""Search by embedding (stub: returns empty)."""
return []
class MemoryService:
"""
Unified memory service with tenant isolation.
Wraps WorkingMemory (session), EpisodicMemory, SemanticMemory, VectorMemory.
"""
def __init__(
self,
tenant_id: str = "default",
user_id: str | None = None,
) -> None:
self._tenant_id = tenant_id
self._user_id = user_id or "anonymous"
self._working = WorkingMemory()
self._episodic = EpisodicMemory()
self._semantic = SemanticMemory()
self._vector = VectorMemory()
@property
def session(self) -> WorkingMemory:
"""Short-term session memory."""
return self._working
@property
def episodic(self) -> EpisodicMemory:
"""Episodic memory (what happened, decisions, outcomes)."""
return self._episodic
@property
def semantic(self) -> SemanticMemory:
"""Semantic memory (facts, preferences)."""
return self._semantic
@property
def vector(self) -> VectorMemory:
"""Vector memory (embeddings for retrieval)."""
return self._vector
def scope_session(self, session_id: str) -> str:
"""Return tenant/user scoped session key."""
return _scoped_key(self._tenant_id, self._user_id, session_id)
def get(self, session_id: str, key: str, default: Any = None) -> Any:
"""Get from session memory (scoped)."""
scoped = self.scope_session(session_id)
return self._working.get(scoped, key, default)
def set(self, session_id: str, key: str, value: Any) -> None:
"""Set in session memory (scoped)."""
scoped = self.scope_session(session_id)
self._working.set(scoped, key, value)
def append_episode(self, task_id: str, event: dict[str, Any], event_type: str | None = None) -> int:
"""Append to episodic memory (with tenant in metadata)."""
event = dict(event)
meta = event.setdefault("metadata", {})
meta = dict(meta) if meta else {}
meta["tenant_id"] = self._tenant_id
meta["user_id"] = self._user_id
event["metadata"] = meta
return self._episodic.append(task_id, event, event_type)

View File

@@ -0,0 +1,79 @@
"""Context sharding: cluster atomic units by semantic similarity or domain."""
from __future__ import annotations
import re
import uuid
from dataclasses import dataclass, field
from typing import Any
from fusionagi.schemas.atomic import AtomicSemanticUnit
@dataclass
class Shard:
"""A cluster of atomic units with optional summary and embedding."""
shard_id: str = field(default_factory=lambda: f"shard_{uuid.uuid4().hex[:12]}")
unit_ids: list[str] = field(default_factory=list)
summary: str = ""
embedding: list[float] | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def _extract_keywords(text: str) -> set[str]:
"""Extract keywords for clustering."""
content = " ".join(text.lower().split())
return set(re.findall(r"\b[a-z0-9]{3,}\b", content))
def _keyword_similarity(a: set[str], b: set[str]) -> float:
"""Jaccard similarity between keyword sets."""
if not a and not b:
return 1.0
inter = len(a & b)
union = len(a | b)
return inter / union if union else 0.0
def _cluster_by_keywords(
units: list[AtomicSemanticUnit],
max_cluster_size: int,
) -> list[list[AtomicSemanticUnit]]:
"""Cluster units by keyword overlap (greedy)."""
if not units:
return []
if len(units) <= max_cluster_size:
return [units]
unit_keywords: list[set[str]] = [_extract_keywords(u.content) for u in units]
clusters: list[list[int]] = []
assigned: set[int] = set()
for i in range(len(units)):
if i in assigned:
continue
cluster = [i]
assigned.add(i)
for j in range(i + 1, len(units)):
if j in assigned or len(cluster) >= max_cluster_size:
continue
sim = _keyword_similarity(unit_keywords[i], unit_keywords[j])
if sim > 0.1:
cluster.append(j)
assigned.add(j)
clusters.append(cluster)
return [[units[idx] for idx in c] for c in clusters]
def shard_context(
units: list[AtomicSemanticUnit],
max_cluster_size: int = 20,
) -> list[Shard]:
"""Shard atomic units into clusters by semantic similarity."""
clusters = _cluster_by_keywords(units, max_cluster_size)
shards: list[Shard] = []
for cluster in clusters:
unit_ids = [u.unit_id for u in cluster]
summary_parts = [u.content[:80] for u in cluster[:3]]
summary = "; ".join(summary_parts) + ("..." if len(cluster) > 3 else "")
shards.append(Shard(unit_ids=unit_ids, summary=summary, metadata={"unit_count": len(cluster)}))
return shards

View File

@@ -0,0 +1,134 @@
"""Versioned thought states: snapshots, rollback, branching."""
from __future__ import annotations
import time
import uuid
from dataclasses import dataclass, field
from typing import Any
from fusionagi.memory.scratchpad import ThoughtState
from fusionagi.reasoning.tot import ThoughtNode
from fusionagi._logger import logger
@dataclass
class ThoughtStateSnapshot:
"""Snapshot of reasoning state: tree + scratchpad."""
version_id: str = field(default_factory=lambda: f"v_{uuid.uuid4().hex[:12]}")
tree_state: dict[str, Any] | None = None
scratchpad_state: ThoughtState | None = None
timestamp: float = field(default_factory=time.monotonic)
metadata: dict[str, Any] = field(default_factory=dict)
def _serialize_tree(node: ThoughtNode | None) -> dict[str, Any]:
"""Serialize ThoughtNode to dict."""
if node is None:
return {}
return {
"node_id": node.node_id,
"parent_id": node.parent_id,
"thought": node.thought,
"trace": node.trace,
"score": node.score,
"depth": node.depth,
"unit_refs": node.unit_refs,
"metadata": node.metadata,
"children": [_serialize_tree(c) for c in node.children],
}
def _deserialize_tree(data: dict) -> ThoughtNode | None:
"""Deserialize dict to ThoughtNode."""
if not data:
return None
node = ThoughtNode(
node_id=data.get("node_id", ""),
parent_id=data.get("parent_id"),
thought=data.get("thought", ""),
trace=data.get("trace", []),
score=float(data.get("score", 0)),
depth=int(data.get("depth", 0)),
unit_refs=list(data.get("unit_refs", [])),
metadata=dict(data.get("metadata", {})),
)
for c in data.get("children", []):
child = _deserialize_tree(c)
if child:
node.children.append(child)
return node
class ThoughtVersioning:
"""Save, load, rollback, branch thought states."""
def __init__(self, max_snapshots: int = 50) -> None:
self._snapshots: dict[str, ThoughtStateSnapshot] = {}
self._max_snapshots = max_snapshots
def save_snapshot(
self,
tree: ThoughtNode | None,
scratchpad: ThoughtState | None,
metadata: dict[str, Any] | None = None,
) -> str:
"""Save snapshot; return version_id."""
snapshot = ThoughtStateSnapshot(
tree_state=_serialize_tree(tree) if tree else {},
scratchpad_state=scratchpad,
metadata=metadata or {},
)
self._snapshots[snapshot.version_id] = snapshot
if len(self._snapshots) > self._max_snapshots:
oldest = min(self._snapshots.keys(), key=lambda k: self._snapshots[k].timestamp)
del self._snapshots[oldest]
logger.debug("Thought snapshot saved", extra={"version_id": snapshot.version_id})
return snapshot.version_id
def load_snapshot(
self,
version_id: str,
) -> tuple[ThoughtNode | None, ThoughtState | None]:
"""Load snapshot; return (tree, scratchpad)."""
snap = self._snapshots.get(version_id)
if not snap:
return None, None
tree = _deserialize_tree(snap.tree_state or {}) if snap.tree_state else None
return tree, snap.scratchpad_state
def list_snapshots(self) -> list[dict[str, Any]]:
"""List available snapshots."""
return [
{
"version_id": v.version_id,
"timestamp": v.timestamp,
"metadata": v.metadata,
}
for v in self._snapshots.values()
]
def rollback_to(
self,
version_id: str,
) -> tuple[ThoughtNode | None, ThoughtState | None]:
"""Load and return snapshot (alias for load_snapshot)."""
return self.load_snapshot(version_id)
def branch_from(
self,
version_id: str,
) -> tuple[ThoughtNode | None, ThoughtState | None]:
"""Branch from snapshot (returns copy for further edits)."""
tree, scratchpad = self.load_snapshot(version_id)
if tree:
tree = _deserialize_tree(_serialize_tree(tree))
if scratchpad:
scratchpad = ThoughtState(
hypotheses=list(scratchpad.hypotheses),
partial_conclusions=list(scratchpad.partial_conclusions),
discarded_paths=list(scratchpad.discarded_paths),
metadata=dict(scratchpad.metadata),
)
return tree, scratchpad

64
fusionagi/memory/trust.py Normal file
View File

@@ -0,0 +1,64 @@
"""Trust memory: verified vs unverified, provenance, confidence decay for AGI."""
from datetime import datetime, timezone
from typing import Any
from fusionagi._logger import logger
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
class TrustMemory:
"""
Tracks verified vs unverified claims, source provenance, and optional
confidence decay over time.
"""
def __init__(self, decay_enabled: bool = False) -> None:
self._entries: dict[str, dict[str, Any]] = {} # claim_id -> {verified, source, confidence, created_at}
self._decay_enabled = decay_enabled
def add(
self,
claim_id: str,
verified: bool,
source: str = "",
confidence: float = 1.0,
metadata: dict[str, Any] | None = None,
) -> None:
"""Record a claim with verification status and provenance."""
self._entries[claim_id] = {
"verified": verified,
"source": source,
"confidence": confidence,
"created_at": _utc_now(),
"metadata": metadata or {},
}
logger.debug("Trust memory: claim added", extra={"claim_id": claim_id, "verified": verified})
def get(self, claim_id: str) -> dict[str, Any] | None:
"""Return trust entry or None. Applies decay if enabled."""
e = self._entries.get(claim_id)
if not e:
return None
if self._decay_enabled:
# Simple decay: reduce confidence by 0.01 per day (placeholder)
from datetime import timedelta
age_days = (_utc_now() - e["created_at"]).total_seconds() / 86400
e = dict(e)
e["confidence"] = max(0.0, e["confidence"] - 0.01 * age_days)
return e
def is_verified(self, claim_id: str) -> bool:
"""Return True if claim is marked verified."""
e = self._entries.get(claim_id)
return e.get("verified", False) if e else False
def set_verified(self, claim_id: str, verified: bool) -> bool:
"""Update verified status. Returns True if claim existed."""
if claim_id not in self._entries:
return False
self._entries[claim_id]["verified"] = verified
return True

View File

@@ -0,0 +1,101 @@
"""pgvector-backed VectorMemory adapter. Requires: pip install fusionagi[vector]."""
from typing import Any
from fusionagi._logger import logger
def create_vector_memory_pgvector(
connection_string: str,
table_name: str = "embeddings",
dimension: int = 1536,
) -> Any:
"""
Create pgvector-backed VectorMemory when pgvector is installed.
Returns None if pgvector/database unavailable.
"""
try:
import pgvector
from pgvector.psycopg import register_vector
except ImportError:
logger.debug("pgvector not installed; use pip install fusionagi[vector]")
return None
try:
import psycopg
except ImportError:
logger.debug("psycopg not installed; use pip install fusionagi[memory]")
return None
return VectorMemoryPgvector(connection_string, table_name, dimension)
class VectorMemoryPgvector:
"""VectorMemory implementation using pgvector."""
def __init__(
self,
connection_string: str,
table_name: str = "embeddings",
dimension: int = 1536,
) -> None:
import pgvector
from pgvector.psycopg import register_vector
self._conn_str = connection_string
self._table = table_name
self._dim = dimension
with psycopg.connect(connection_string) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
cur.execute(
f"""
CREATE TABLE IF NOT EXISTS {table_name} (
id TEXT PRIMARY KEY,
embedding vector({dimension}),
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
)
"""
)
conn.commit()
def add(self, id: str, embedding: list[float], metadata: dict[str, Any] | None = None) -> None:
import json
import psycopg
from pgvector.psycopg import register_vector
with psycopg.connect(self._conn_str) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute(
f"""
INSERT INTO {self._table} (id, embedding, metadata)
VALUES (%s, %s, %s)
ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, metadata = EXCLUDED.metadata
""",
(id, embedding, json.dumps(metadata or {})),
)
conn.commit()
def search(self, query_embedding: list[float], top_k: int = 10) -> list[dict[str, Any]]:
import json
import psycopg
from pgvector.psycopg import register_vector
with psycopg.connect(self._conn_str) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute(
f"""
SELECT id, metadata
FROM {self._table}
ORDER BY embedding <-> %s
LIMIT %s
""",
(query_embedding, top_k),
)
rows = cur.fetchall()
return [{"id": r[0], "metadata": json.loads(r[1]) if r[1] else {}} for r in rows]

150
fusionagi/memory/working.py Normal file
View File

@@ -0,0 +1,150 @@
"""Working memory: in-memory key-value / list per task/session.
Working memory provides short-term storage for active tasks:
- Key-value storage per session/task
- List append operations for accumulating results
- Context retrieval for reasoning
- Session lifecycle management
"""
from collections import defaultdict
from datetime import datetime
from typing import Any, Iterator
from fusionagi._logger import logger
from fusionagi._time import utc_now
class WorkingMemory:
"""
Short-term working memory per task/session.
Features:
- Key-value get/set operations
- List append with automatic coercion
- Context summary for LLM prompts
- Session management and cleanup
- Size limits to prevent unbounded growth
"""
def __init__(self, max_entries_per_session: int = 1000) -> None:
"""
Initialize working memory.
Args:
max_entries_per_session: Maximum entries per session before oldest are removed.
"""
self._store: dict[str, dict[str, Any]] = defaultdict(dict)
self._timestamps: dict[str, datetime] = {}
self._max_entries = max_entries_per_session
def get(self, session_id: str, key: str, default: Any = None) -> Any:
"""Get value for session and key; returns default if not found."""
return self._store[session_id].get(key, default)
def set(self, session_id: str, key: str, value: Any) -> None:
"""Set value for session and key."""
self._store[session_id][key] = value
self._timestamps[session_id] = utc_now()
self._enforce_limits(session_id)
def append(self, session_id: str, key: str, value: Any) -> None:
"""Append to list for session and key (creates list if needed)."""
if key not in self._store[session_id]:
self._store[session_id][key] = []
lst = self._store[session_id][key]
if not isinstance(lst, list):
lst = [lst]
self._store[session_id][key] = lst
lst.append(value)
self._timestamps[session_id] = utc_now()
self._enforce_limits(session_id)
def get_list(self, session_id: str, key: str) -> list[Any]:
"""Return list for session and key (copy)."""
val = self._store[session_id].get(key)
if isinstance(val, list):
return list(val)
return [val] if val is not None else []
def has(self, session_id: str, key: str) -> bool:
"""Check if a key exists in session."""
return key in self._store.get(session_id, {})
def keys(self, session_id: str) -> list[str]:
"""Return all keys for a session."""
return list(self._store.get(session_id, {}).keys())
def delete(self, session_id: str, key: str) -> bool:
"""Delete a key from session. Returns True if existed."""
if session_id in self._store and key in self._store[session_id]:
del self._store[session_id][key]
return True
return False
def clear_session(self, session_id: str) -> None:
"""Clear all data for a session."""
self._store.pop(session_id, None)
self._timestamps.pop(session_id, None)
def get_context_summary(self, session_id: str, max_items: int = 10) -> dict[str, Any]:
"""
Get a summary of working memory for context injection.
Useful for including relevant context in LLM prompts.
"""
session_data = self._store.get(session_id, {})
summary = {}
for key, value in list(session_data.items())[:max_items]:
if isinstance(value, list):
# For lists, include count and last few items
summary[key] = {
"type": "list",
"count": len(value),
"recent": value[-3:] if len(value) > 3 else value,
}
elif isinstance(value, dict):
# For dicts, include keys
summary[key] = {
"type": "dict",
"keys": list(value.keys())[:10],
}
else:
# For scalars, include the value (truncated if string)
if isinstance(value, str) and len(value) > 200:
summary[key] = value[:200] + "..."
else:
summary[key] = value
return summary
def get_all(self, session_id: str) -> dict[str, Any]:
"""Return all data for a session (copy)."""
return dict(self._store.get(session_id, {}))
def session_exists(self, session_id: str) -> bool:
"""Check if a session has any data."""
return session_id in self._store and bool(self._store[session_id])
def active_sessions(self) -> list[str]:
"""Return list of sessions with data."""
return [sid for sid, data in self._store.items() if data]
def session_count(self) -> int:
"""Return number of active sessions."""
return len([s for s in self._store.values() if s])
def _enforce_limits(self, session_id: str) -> None:
"""Enforce size limits on session data."""
session_data = self._store.get(session_id, {})
total_items = sum(
len(v) if isinstance(v, (list, dict)) else 1
for v in session_data.values()
)
if total_items > self._max_entries:
logger.warning(
"Working memory size limit exceeded",
extra={"session_id": session_id, "items": total_items},
)