{
  "openapi": "3.0.3",
  "info": {
    "title": "KnowledgeChain API",
    "description": "KnowledgeChain REST API — 让 AI Agent 有记忆，做过的事不重复，踩过的坑不再踩。",
    "version": "0.1.0",
    "contact": {
      "name": "KnowledgeChain",
      "url": "https://github.com/knowledge-chain/kc"
    }
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "Current server"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/search": {
      "post": {
        "operationId": "searchExperience",
        "summary": "Search experiences",
        "description": "Semantic search over the experience store. Returns ranked matches with optional summary or full detail.",
        "tags": ["experience"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              },
              "example": {
                "query": "how to handle HTTP errors in Go",
                "limit": 5,
                "detail": "summary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Matching experiences",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/record": {
      "post": {
        "operationId": "recordExperience",
        "summary": "Record a new experience",
        "description": "Saves a completed task as an experience. The engine extracts embeddings, computes quality score, and deduplicates.",
        "tags": ["experience"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordRequest"
              },
              "example": {
                "task_description": "deploy Go service with Docker",
                "approach": ["write Dockerfile", "build image", "run container"],
                "outcome": "success",
                "tags": ["go", "docker", "deployment"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Experience created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query": {
      "post": {
        "operationId": "queryKnowledge",
        "summary": "Query the knowledge graph",
        "description": "Natural language Q&A over the knowledge graph powered by Graphiti.",
        "tags": ["knowledge"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QueryRequest"
              },
              "example": {
                "question": "What is the best way to use Redis for caching?",
                "mode": "hybrid"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Knowledge graph answer",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QueryResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/compare": {
      "post": {
        "operationId": "compareApproaches",
        "summary": "Compare multiple approaches",
        "description": "Multi-signal reasoning layer — retrieves evidence for each candidate approach and ranks them by reliability.",
        "tags": ["reasoning"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompareRequest"
              },
              "example": {
                "problem": "how to implement rate limiting in Go",
                "candidates": ["token bucket", "leaky bucket", "sliding window"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ranked candidates with evidence",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompareResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/expand": {
      "post": {
        "operationId": "expandExperience",
        "summary": "Expand experience details",
        "description": "Given a list of experience IDs (e.g. from a summary search), returns the full approach, anti_patterns, and metrics for each.",
        "tags": ["experience"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExpandRequest"
              },
              "example": {
                "ids": ["exp-001", "exp-002"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Full experience details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExpandResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/relations": {
      "post": {
        "operationId": "searchRelations",
        "summary": "Search REFERENCED_BY edges between experiences",
        "description": "Returns semantic-causal relationships between experiences (e.g. 'A references B because Y'). Maps to MCP tool search_relations.",
        "tags": ["network"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "query": { "type": "string" }, "limit": { "type": "integer", "default": 10 } } },
              "example": { "query": "依赖关系", "limit": 10 }
            }
          }
        },
        "responses": { "200": { "description": "Matching relations", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/episodes/search": {
      "post": {
        "operationId": "searchEpisode",
        "summary": "Trace experience back to raw Agent context (Episode)",
        "description": "Retrieves Episode nodes — the raw agent context log from which experiences were derived. Maps to MCP tool search_episode.",
        "tags": ["network"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "query": { "type": "string" }, "agent_id": { "type": "string" } } },
              "example": { "query": "用户反馈", "agent_id": "founder-001" }
            }
          }
        },
        "responses": { "200": { "description": "Matching episodes", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/communities/search": {
      "post": {
        "operationId": "searchCommunities",
        "summary": "Community-level retrieval (match knowledge community → rank members)",
        "description": "Two-stage: first match knowledge community by query, then rank members within. Maps to MCP tool search_communities.",
        "tags": ["network"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "query": { "type": "string" }, "members_per_community": { "type": "integer", "default": 5 } } },
              "example": { "query": "团队管理", "members_per_community": 5 }
            }
          }
        },
        "responses": { "200": { "description": "Matching communities + members", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/consult": {
      "post": {
        "operationId": "advisorConsult",
        "summary": "Advisor planning entry (search + news + multi-approach compare)",
        "description": "Comprehensive planning endpoint — fuses search + news + multi-approach comparison into a single advisor response. Maps to MCP tool advisor_consult.",
        "tags": ["advisor"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "topic": { "type": "string" }, "include_news": { "type": "boolean", "default": true } }, "required": ["topic"] },
              "example": { "topic": "如何选择向量数据库", "include_news": true }
            }
          }
        },
        "responses": { "200": { "description": "Advisor response", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/news/today": {
      "post": {
        "operationId": "newsToday",
        "summary": "Today's AI industry news (isolated source so search isn't polluted)",
        "description": "Returns daily AI news, isolated by source=ai-news so news content does not pollute experience search. Maps to MCP tool news_today.",
        "tags": ["news"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "topics": { "type": "array", "items": { "type": "string" } }, "exclude_tags": { "type": "array", "items": { "type": "string" } } } },
              "example": { "date": "2026-05-11", "topics": ["LLM", "agents"] }
            }
          }
        },
        "responses": { "200": { "description": "Today news items", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    },
    "/news/ingest/status": {
      "get": {
        "operationId": "getNewsIngestStatus",
        "summary": "News Ingest worker status (last run, item count, triggered_by)",
        "description": "Returns the state of the News Ingest worker — last run time, item count, passed count, triggered_by source.",
        "tags": ["news"],
        "responses": { "200": { "description": "Ingest status", "content": { "application/json": { "schema": { "type": "object", "properties": { "enabled": { "type": "boolean" }, "last_ingest_at": { "type": "string", "format": "date-time" }, "item_count": { "type": "integer" }, "passed_count": { "type": "integer" }, "triggered_by": { "type": "string" } } } } } } }
      }
    },
    "/news/ingest/trigger": {
      "post": {
        "operationId": "triggerNewsIngest",
        "summary": "Manually trigger a News Ingest cycle (cross-process via PG NOTIFY)",
        "description": "Triggers an immediate news ingest cycle. Cross-process: API writes to PG, worker LISTEN-s and runs. Returns immediately after notify queued.",
        "tags": ["news"],
        "responses": { "200": { "description": "Trigger accepted", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "triggered" } } } } } } }
      }
    },
    "/record/observation": {
      "post": {
        "operationId": "recordObservation",
        "summary": "Save an Observation KU (raw external signal / news / blog claim)",
        "description": "Plan ⑦ Group D dual-track ingest. Saves a raw external signal as an Observation KU. Source URL or source_authority lifts to active; otherwise lands draft. Gated by KC_MCP_RECORD_V3_ENABLED.",
        "tags": ["ingest"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RecordObservationRequest" },
              "example": {
                "subject": "Anthropic releases Claude 4.7 with 1M context",
                "raw_content": "On 2026-05-10 Anthropic announced...",
                "source_url": "https://www.anthropic.com/news/claude-4-7",
                "source_authority": "primary"
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Observation saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordKUResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/record/question": {
      "post": {
        "operationId": "recordQuestion",
        "summary": "Save a Question KU (open knowledge gap)",
        "description": "Plan ⑦ Group D dual-track ingest. Logs an open question. Re-asking within 24h bumps asked_count via IncrementAskedCount instead of minting a new node — that powers the question_v1 leverage formula. Gated by KC_MCP_RECORD_V3_ENABLED.",
        "tags": ["ingest"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RecordQuestionRequest" },
              "example": {
                "question_text": "What's the cheapest GPU config for Qwen3-Embedding-8B at 1k req/s?",
                "context": "We're sizing the inference cluster",
                "expected_answer_type": "fact"
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Question saved or merged (asked_count++)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordKUResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/record/benchmark": {
      "post": {
        "operationId": "recordBenchmark",
        "summary": "Save a Benchmark KU (performance measurement)",
        "description": "Plan ⑦ Group D dual-track ingest. Strict env_signature_rule — all 4 axes (runtime / runtime_version / platform / framework_versions) required for auto-active. Missing axes land draft with reason. Gated by KC_MCP_RECORD_V3_ENABLED.",
        "tags": ["ingest"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RecordBenchmarkRequest" },
              "example": {
                "metric": "p95_latency_ms",
                "value": 120.5,
                "config": {"batch_size": 32},
                "env_fingerprint": {
                  "runtime": "python",
                  "runtime_version": "3.12",
                  "platform": "linux-amd64",
                  "framework_versions": {"transformers": "4.40.0"}
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Benchmark saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordKUResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/record/constraint": {
      "post": {
        "operationId": "recordConstraint",
        "summary": "Save a Constraint KU (normative rule / red line)",
        "description": "Plan ⑦ Group D dual-track ingest. Severity policy: hard requires ≥2 derived_from evidence for auto-active; soft works best with ≥1. Each derived_from emits a kind=derived classification event so mv_emergence_leverage's per-Pattern counter bumps. Gated by KC_MCP_RECORD_V3_ENABLED.",
        "tags": ["ingest"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RecordConstraintRequest" },
              "example": {
                "rule_text": "never hard-delete demoted KUs — only status transitions",
                "severity": "hard",
                "scope": "storage layer",
                "derived_from": ["pat_demote_001", "pat_demote_002"]
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Constraint saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordKUResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key as Bearer token. Omit in dev mode (API_KEYS env var empty)."
      }
    },
    "schemas": {
      "SearchRequest": {
        "type": "object",
        "required": ["query"],
        "properties": {
          "query": {
            "type": "string",
            "description": "Natural language query"
          },
          "limit": {
            "type": "integer",
            "default": 5,
            "minimum": 1,
            "maximum": 20,
            "description": "Max number of results"
          },
          "detail": {
            "type": "string",
            "enum": ["summary", "full"],
            "default": "summary",
            "description": "summary saves ~80% tokens; use expand to fetch full details"
          },
          "tags": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Optional tag filters"
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MatchResult"
            }
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "MatchResult": {
        "type": "object",
        "properties": {
          "experience_id": {"type": "string"},
          "task_description": {"type": "string"},
          "match_score": {"type": "number", "format": "float"},
          "semantic_score": {"type": "number", "format": "float"},
          "source": {"type": "string"},
          "reflection": {
            "type": "string",
            "enum": ["accept", "warn", "refine"],
            "description": "Self-RAG style signal — accept: use as-is; warn: use with caution; refine: consider re-querying"
          },
          "tags": {
            "type": "array",
            "items": {"type": "string"}
          },
          "quality_score": {"type": "number", "format": "float"},
          "status": {"type": "string"}
        }
      },
      "RecordRequest": {
        "type": "object",
        "required": ["task_description", "approach"],
        "properties": {
          "task_description": {
            "type": "string",
            "description": "What the agent was trying to do"
          },
          "approach": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Ordered steps taken"
          },
          "outcome": {
            "type": "string",
            "enum": ["success", "failure", "partial"],
            "default": "success"
          },
          "tags": {
            "type": "array",
            "items": {"type": "string"}
          },
          "anti_patterns": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Pitfalls to avoid next time"
          },
          "referenced_experience_id": {
            "type": "string",
            "description": "ID of an experience this builds on (provenance chain)"
          }
        }
      },
      "RecordResponse": {
        "type": "object",
        "properties": {
          "experience_id": {"type": "string"},
          "status": {"type": "string"},
          "quality_score": {"type": "number", "format": "float"},
          "duplicate_of": {
            "type": "string",
            "description": "Set if this experience was merged into an existing one"
          }
        }
      },
      "QueryRequest": {
        "type": "object",
        "required": ["question"],
        "properties": {
          "question": {
            "type": "string",
            "description": "Natural language question"
          },
          "context": {
            "type": "string",
            "description": "Optional extra context to scope the query"
          }
        }
      },
      "QueryResponse": {
        "type": "object",
        "properties": {
          "answer": {"type": "string"},
          "confidence": {"type": "number", "format": "float"},
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {"type": "string"},
                "content": {"type": "string"},
                "score": {"type": "number", "format": "float"}
              }
            }
          }
        }
      },
      "CompareRequest": {
        "type": "object",
        "required": ["problem"],
        "properties": {
          "problem": {
            "type": "string",
            "description": "The problem or decision to compare approaches for"
          },
          "candidates": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Approach names to compare. If omitted, the engine retrieves candidates from the experience store."
          }
        }
      },
      "CompareResponse": {
        "type": "object",
        "properties": {
          "candidates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "approach": {"type": "string"},
                "score": {"type": "number", "format": "float"},
                "evidence_count": {"type": "integer"},
                "summary": {"type": "string"}
              }
            }
          },
          "recommendation": {"type": "string"}
        }
      },
      "ExpandRequest": {
        "type": "object",
        "required": ["ids"],
        "properties": {
          "ids": {
            "type": "array",
            "items": {"type": "string"},
            "minItems": 1,
            "description": "Experience IDs to expand (from a previous search_experience call)"
          }
        }
      },
      "ExpandResponse": {
        "type": "object",
        "properties": {
          "experiences": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExperienceFull"
            }
          }
        }
      },
      "ExperienceFull": {
        "type": "object",
        "properties": {
          "id": {"type": "string"},
          "task_description": {"type": "string"},
          "approach": {
            "type": "array",
            "items": {"type": "string"}
          },
          "outcome": {"type": "string"},
          "anti_patterns": {
            "type": "array",
            "items": {"type": "string"}
          },
          "tags": {
            "type": "array",
            "items": {"type": "string"}
          },
          "quality_score": {"type": "number", "format": "float"},
          "confidence": {"type": "number", "format": "float"},
          "reuse_count": {"type": "integer"},
          "status": {"type": "string"},
          "created_at": {"type": "string", "format": "date-time"},
          "updated_at": {"type": "string", "format": "date-time"}
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {"type": "string"}
        }
      },
      "RecordObservationRequest": {
        "type": "object",
        "required": ["subject", "raw_content"],
        "properties": {
          "agent_id": {"type": "string", "description": "Caller agent id (Sybil — empty falls back to anon_<hex>)."},
          "group_id": {"type": "string", "description": "Tenancy filter. Empty → global."},
          "subject": {"type": "string", "description": "One-line headline of what was observed."},
          "raw_content": {"type": "string", "description": "Full observation text or quoted block."},
          "source_url": {"type": "string", "description": "Canonical URL of the source. Lifts observation to active."},
          "source_authority": {"type": "string", "description": "Provenance tier — primary / secondary / aggregator."},
          "observed_at": {"type": "string", "format": "date-time"},
          "ingestion_origin": {"type": "string", "description": "Producer pipeline tag. Defaults to agent_record. ai-news isolates from Experience search."}
        }
      },
      "RecordQuestionRequest": {
        "type": "object",
        "required": ["question_text"],
        "properties": {
          "agent_id": {"type": "string"},
          "group_id": {"type": "string"},
          "question_text": {"type": "string"},
          "context": {"type": "string", "description": "Optional surrounding context (file path, task summary, error message)."},
          "expected_answer_type": {"type": "string", "enum": ["", "fact", "hypothesis", "skill"], "description": "What kind of KU should resolve this gap."}
        }
      },
      "RecordBenchmarkRequest": {
        "type": "object",
        "required": ["metric", "value"],
        "properties": {
          "agent_id": {"type": "string"},
          "group_id": {"type": "string"},
          "metric": {"type": "string"},
          "value": {"type": "number"},
          "config": {"type": "object"},
          "hardware": {"type": "object"},
          "measured_at": {"type": "string", "format": "date-time"},
          "env_fingerprint": {
            "type": "object",
            "description": "All 4 axes required for active (strict env_signature_rule).",
            "properties": {
              "runtime": {"type": "string"},
              "runtime_version": {"type": "string"},
              "platform": {"type": "string"},
              "arch": {"type": "string"},
              "framework_versions": {"type": "object", "additionalProperties": {"type": "string"}},
              "agent_type": {"type": "string"}
            }
          },
          "source_experience_id": {"type": "string"}
        }
      },
      "RecordConstraintRequest": {
        "type": "object",
        "required": ["rule_text", "severity"],
        "properties": {
          "agent_id": {"type": "string"},
          "group_id": {"type": "string"},
          "rule_text": {"type": "string"},
          "severity": {"type": "string", "enum": ["hard", "soft"]},
          "scope": {"type": "string"},
          "derived_from": {
            "type": "array",
            "items": {"type": "string"},
            "description": "Supporting Pattern/Experience IDs. severity=hard requires ≥2; severity=soft works best with ≥1."
          }
        }
      },
      "RecordKUResponse": {
        "type": "object",
        "properties": {
          "ku_id": {"type": "string"},
          "type_name": {"type": "string"},
          "stored": {"type": "string", "enum": ["local", "merged"], "description": "local = new KU saved; merged = 24h dedup hit, existing KU returned."},
          "status": {"type": "string", "description": "KU lifecycle state — draft / active / etc."},
          "quality_score": {"type": "number", "format": "float"},
          "reason": {"type": "string", "description": "Optional explanation when severity/env policy downgraded the row to draft."}
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": {
          "application/json": {
            "schema": {"$ref": "#/components/schemas/ErrorResponse"}
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {"$ref": "#/components/schemas/ErrorResponse"}
          }
        }
      }
    }
  }
}