""" 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), }