"""
Knowledge Graph HTTP API — FastAPI server on localhost:8333.
All agents can write/query the knowledge graph through this API.
"""
import json
import logging
from typing import Optional
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field, field_validator
from graph import (
add_entity, add_relation, get_entity, search_entities,
explore, merge_entities, decay_stale, infer_relations,
health, VALID_ENTITY_TYPES, VALID_RELATION_TYPES, VALID_SOURCES,
)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("knowledge_api")
app = FastAPI(
title="Knowledge Graph API",
description="Structured memory with SQLite + FAISS vector index",
version="1.0.0",
)
──────────────────────────────────────────────
Pydantic models
──────────────────────────────────────────────
class EntityCreate(BaseModel):
type: str = Field(..., description="Entity type")
subject: str = Field(..., min_length=1, description="Entity subject/name")
description: str = Field(default="", description="Description text")
aliases: list[str] = Field(default_factory=list, description="Alternative names")
tags: list[str] = Field(default_factory=list, description="Tags")
confidence: float = Field(default=1.0, ge=0, le=1, description="Confidence 0-1")
source: str = Field(default="live", description="Source type")
@field_validator("type")
@classmethod
def validate_type(cls, v):
if v not in VALID_ENTITY_TYPES:
raise ValueError(f"Invalid type '{v}'. Must be one of: {', '.join(sorted(VALID_ENTITY_TYPES))}")
return v
@field_validator("source")
@classmethod
def validate_source(cls, v):
if v not in VALID_SOURCES:
raise ValueError(f"Invalid source '{v}'. Must be one of: {', '.join(sorted(VALID_SOURCES))}")
return v
class RelationCreate(BaseModel):
source_entity_id: str = Field(..., description="Source entity UUID")
target_entity_id: str = Field(..., description="Target entity UUID")
relation_type: str = Field(..., description="Relation type from controlled vocabulary")
confidence: float = Field(default=1.0, ge=0, le=1, description="Confidence 0-1")
source: str = Field(default="live", description="Source type")
@field_validator("relation_type")
@classmethod
def validate_relation_type(cls, v):
from graph import VAGUE_RELATION_TYPES, ValidationError
if v in VAGUE_RELATION_TYPES:
raise ValueError(
f"Vague relation type '{v}' rejected. Use a specific type from: "
f"{', '.join(sorted(VALID_RELATION_TYPES))}"
)
if v not in VALID_RELATION_TYPES:
raise ValueError(
f"Invalid relation type '{v}'. Must be one of: "
f"{', '.join(sorted(VALID_RELATION_TYPES))}"
)
return v
@field_validator("source")
@classmethod
def validate_source(cls, v):
if v not in VALID_SOURCES:
raise ValueError(f"Invalid source '{v}'. Must be one of: {', '.join(sorted(VALID_SOURCES))}")
return v
class MergeRequest(BaseModel):
entity_id_1: str = Field(..., description="Entity to keep")
entity_id_2: str = Field(..., description="Entity to merge into entity_1 and delete")
class DecayRequest(BaseModel):
days: int = Field(default=90, ge=1, description="Age threshold in days")
decay_rate: float = Field(default=0.1, ge=0, le=1, description="Confidence decay amount")
──────────────────────────────────────────────
Endpoints
──────────────────────────────────────────────
@app.get("/health")
def get_health():
return health()
@app.post("/entity")
def create_entity(data: EntityCreate):
"""Add a new entity to the knowledge graph."""
try:
entity_id = add_entity(
type_=data.type,
subject=data.subject,
description=data.description,
aliases=data.aliases,
tags=data.tags,
confidence=data.confidence,
source=data.source,
)
return {"entity_id": entity_id, "status": "created"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/relation")
def create_relation(data: RelationCreate):
"""Add a relation between two entities."""
try:
relation_id = add_relation(
source_id=data.source_entity_id,
target_id=data.target_entity_id,
relation_type=data.relation_type,
confidence=data.confidence,
source=data.source,
)
return {"relation_id": relation_id, "status": "created"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/search")
def search(q: str = Query(..., description="Search query"), top_k: int = Query(5, description="Number of results")):
"""Semantic search over entity descriptions."""
try:
results = search_entities(q, top_k)
return {"query": q, "results": results, "count": len(results)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/entity/{entity_id}")
def read_entity(entity_id: str):
"""Get an entity with all its relations."""
try:
entity = get_entity(entity_id)
if not entity:
raise HTTPException(status_code=404, detail="Entity not found")
return entity
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/explore/{entity_id}")
def explore_entity(entity_id: str, depth: int = Query(1, ge=1, description="Traversal depth")):
"""Graph traversal — returns entity + neighbors."""
try:
result = explore(entity_id, depth=depth)
if not result:
raise HTTPException(status_code=404, detail="Entity not found")
return result
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/merge")
def merge(data: MergeRequest):
"""Merge two entities. Entity 1 absorbs entity 2."""
try:
result = merge_entities(data.entity_id_1, data.entity_id_2)
return result
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/decay")
def decay(data: DecayRequest = DecayRequest()):
"""Decay confidence of old dream/extraction relations."""
try:
result = decay_stale(days=data.days, decay_rate=data.decay_rate)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/infer")
def infer():
"""Find entity pairs with shared tags but no relation."""
try:
result = infer_relations()
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/vocabulary")
def get_vocabulary():
"""Get the controlled relation vocabulary and entity types."""
return {
"entity_types": sorted(VALID_ENTITY_TYPES),
"relation_types": sorted(VALID_RELATION_TYPES),
}