|
|
|
|
|
|
|
|
from fastapi import FastAPI, Request, HTTPException |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from flask import Flask, request, jsonify |
|
|
from pydantic import BaseModel |
|
|
from typing import List, Optional |
|
|
from datetime import datetime |
|
|
from models import GraphExport |
|
|
from storage import Storage |
|
|
from tools.concept_store import ConceptStore |
|
|
from tools.notebook_store import NotebookStore |
|
|
import random |
|
|
|
|
|
app = FastAPI(title="HMP MCP-Agent API", version="0.1") |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
concept_store = ConceptStore() |
|
|
notebook_store = NotebookStore() |
|
|
db = Storage() |
|
|
|
|
|
|
|
|
|
|
|
class EntryInput(BaseModel): |
|
|
text: str |
|
|
tags: Optional[List[str]] = [] |
|
|
timestamp: Optional[str] = None |
|
|
|
|
|
class EntryOutput(BaseModel): |
|
|
id: int |
|
|
text: str |
|
|
tags: List[str] |
|
|
timestamp: str |
|
|
|
|
|
class EntryListOutput(BaseModel): |
|
|
entries: List[EntryOutput] |
|
|
|
|
|
class ConceptInput(BaseModel): |
|
|
name: str |
|
|
description: Optional[str] = None |
|
|
|
|
|
class ConceptOutput(BaseModel): |
|
|
concept_id: int |
|
|
|
|
|
class LinkInput(BaseModel): |
|
|
source_id: int |
|
|
target_id: int |
|
|
relation: str |
|
|
|
|
|
class LinkOutput(BaseModel): |
|
|
link_id: int |
|
|
|
|
|
class Node(BaseModel): |
|
|
id: str |
|
|
label: str |
|
|
tags: List[str] = [] |
|
|
|
|
|
class Edge(BaseModel): |
|
|
source: str |
|
|
target: str |
|
|
relation: str |
|
|
|
|
|
class GraphImportData(BaseModel): |
|
|
nodes: List[Node] = [] |
|
|
edges: List[Edge] = [] |
|
|
|
|
|
class GraphExpansionOutput(BaseModel): |
|
|
links: List[Edge] |
|
|
|
|
|
class Concept(BaseModel): |
|
|
concept_id: int |
|
|
name: str |
|
|
description: Optional[str] = None |
|
|
|
|
|
class ConceptQueryOutput(BaseModel): |
|
|
matches: List[Concept] |
|
|
|
|
|
class DiaryEntry(BaseModel): |
|
|
id: int |
|
|
text: str |
|
|
tags: List[str] |
|
|
timestamp: str |
|
|
|
|
|
class DiaryExport(BaseModel): |
|
|
entries: List[DiaryEntry] |
|
|
|
|
|
class ConceptExport(BaseModel): |
|
|
id: int |
|
|
name: str |
|
|
description: Optional[str] = None |
|
|
|
|
|
class LinkExport(BaseModel): |
|
|
id: int |
|
|
source_id: int |
|
|
target_id: int |
|
|
relation: str |
|
|
|
|
|
class GraphExport(BaseModel): |
|
|
concepts: List[ConceptExport] |
|
|
links: List[LinkExport] |
|
|
|
|
|
class ConceptUpdate(BaseModel): |
|
|
name: Optional[str] = None |
|
|
description: Optional[str] = None |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/status") |
|
|
def status(): |
|
|
return { |
|
|
"status": "ok", |
|
|
"agent": "HMP-MCP", |
|
|
"timestamp": datetime.utcnow().isoformat() |
|
|
} |
|
|
|
|
|
@app.post("/write_entry", response_model=dict) |
|
|
def write_entry(entry: EntryInput): |
|
|
db.write_entry(entry.text, entry.tags) |
|
|
return {"result": "entry saved"} |
|
|
|
|
|
@app.get("/read_entries", response_model=EntryListOutput) |
|
|
def read_entries(limit: int = 5, tag: Optional[str] = None): |
|
|
raw = db.read_entries(limit=limit, tag_filter=tag) |
|
|
return { |
|
|
"entries": [ |
|
|
{ |
|
|
"id": r[0], |
|
|
"text": r[1], |
|
|
"tags": r[2].split(",") if r[2] else [], |
|
|
"timestamp": r[3] |
|
|
} for r in raw |
|
|
] |
|
|
} |
|
|
|
|
|
@app.get("/") |
|
|
def root(): |
|
|
return {"message": "HMP MCP-Agent API is running"} |
|
|
|
|
|
@app.post("/add_concept", response_model=ConceptOutput) |
|
|
def add_concept(concept: ConceptInput): |
|
|
cid = db.add_concept(concept.name, concept.description) |
|
|
return {"concept_id": cid} |
|
|
|
|
|
@app.post("/add_link", response_model=LinkOutput) |
|
|
def add_link(link: LinkInput): |
|
|
link_id = db.add_link(link.source_id, link.target_id, link.relation) |
|
|
return {"link_id": link_id} |
|
|
|
|
|
@app.get("/expand_graph", response_model=GraphExpansionOutput) |
|
|
def expand_graph(start_id: int, depth: int = 1): |
|
|
raw_links = db.expand_graph(start_id, depth) |
|
|
edges = [{"source_id": s, "target_id": t, "relation": r} for s, t, r in raw_links] |
|
|
return {"links": edges} |
|
|
|
|
|
@app.get("/query_concept", response_model=ConceptQueryOutput) |
|
|
def query_concept(name: str): |
|
|
results = db.query_concept(name) |
|
|
return { |
|
|
"matches": [ |
|
|
{"concept_id": row[0], "name": row[1], "description": row[2]} |
|
|
for row in results |
|
|
] |
|
|
} |
|
|
|
|
|
@app.get("/list_concepts", response_model=List[Concept]) |
|
|
def list_concepts(): |
|
|
rows = db.list_concepts() |
|
|
return [ |
|
|
{"concept_id": row[0], "name": row[1], "description": row[2]} |
|
|
for row in rows |
|
|
] |
|
|
|
|
|
@app.get("/list_links", response_model=List[Edge]) |
|
|
def list_links(): |
|
|
rows = db.list_links() |
|
|
return [ |
|
|
{"source_id": row[1], "target_id": row[2], "relation": row[3]} |
|
|
for row in rows |
|
|
] |
|
|
|
|
|
@app.delete("/delete_concept/{concept_id}") |
|
|
def delete_concept(concept_id: int): |
|
|
db.delete_concept(concept_id) |
|
|
return {"result": f"concept {concept_id} deleted"} |
|
|
|
|
|
@app.delete("/delete_link/{link_id}") |
|
|
def delete_link(link_id: int): |
|
|
db.delete_link(link_id) |
|
|
return {"result": f"link {link_id} deleted"} |
|
|
|
|
|
@app.delete("/delete_entry/{entry_id}") |
|
|
def delete_entry(entry_id: int): |
|
|
db.delete_entry(entry_id) |
|
|
return {"result": f"entry {entry_id} deleted"} |
|
|
|
|
|
@app.get("/export_diary", response_model=DiaryExport) |
|
|
def export_diary(): |
|
|
rows = db.export_diary() |
|
|
return { |
|
|
"entries": [ |
|
|
{ |
|
|
"id": r[0], |
|
|
"text": r[1], |
|
|
"tags": r[2].split(",") if r[2] else [], |
|
|
"timestamp": r[3] |
|
|
} |
|
|
for r in rows |
|
|
] |
|
|
} |
|
|
|
|
|
@app.get("/export_graph", response_model=GraphExport) |
|
|
def export_graph(): |
|
|
return concept_store.export_as_json() |
|
|
|
|
|
@app.put("/update_concept/{concept_id}") |
|
|
def update_concept(concept_id: int, update: ConceptUpdate): |
|
|
db.update_concept(concept_id, update.name, update.description) |
|
|
return {"result": f"concept {concept_id} updated"} |
|
|
|
|
|
@app.get("/tag_stats", response_model=dict) |
|
|
def tag_stats(): |
|
|
return db.get_tag_stats() |
|
|
|
|
|
@app.get("/search_links", response_model=List[LinkExport]) |
|
|
def search_links(relation: str): |
|
|
rows = db.search_links_by_relation(relation) |
|
|
return [ |
|
|
{ |
|
|
"id": row[0], |
|
|
"source_id": row[1], |
|
|
"target_id": row[2], |
|
|
"relation": row[3] |
|
|
} |
|
|
for row in rows |
|
|
] |
|
|
|
|
|
@app.get("/search_concepts", response_model=List[Concept]) |
|
|
def search_concepts(query: str): |
|
|
results = db.search_concepts(query) |
|
|
return [ |
|
|
{"concept_id": row[0], "name": row[1], "description": row[2]} |
|
|
for row in results |
|
|
] |
|
|
|
|
|
@app.post("/merge_concepts", response_model=dict) |
|
|
def merge_concepts(source_id: int, target_id: int): |
|
|
db.merge_concepts(source_id, target_id) |
|
|
return {"result": f"concept {source_id} merged into {target_id}"} |
|
|
|
|
|
@app.post("/relate_concepts", response_model=LinkOutput) |
|
|
def relate_concepts(source_name: str, target_name: str, relation: str): |
|
|
sid = db.find_concept_id_by_name(source_name) |
|
|
tid = db.find_concept_id_by_name(target_name) |
|
|
if sid is None or tid is None: |
|
|
raise HTTPException(status_code=404, detail="Concept not found") |
|
|
link_id = db.add_link(sid, tid, relation) |
|
|
return {"link_id": link_id} |
|
|
|
|
|
@app.get("/tag_cloud", response_model=dict) |
|
|
def tag_cloud(): |
|
|
return db.get_tag_stats() |
|
|
|
|
|
@app.get("/get_concept/{concept_id}") |
|
|
def get_concept(concept_id: str): |
|
|
concept = concept_store.get(concept_id) |
|
|
if concept: |
|
|
return concept |
|
|
raise HTTPException(status_code=404, detail="Concept not found") |
|
|
|
|
|
@app.get("/get_entry/{entry_id}") |
|
|
def get_entry(entry_id: str): |
|
|
entry = notebook_store.get(entry_id) |
|
|
if entry: |
|
|
return entry |
|
|
raise HTTPException(status_code=404, detail="Entry not found") |
|
|
|
|
|
@app.post("/search_entries") |
|
|
def search_entries(query: str): |
|
|
results = notebook_store.search(query) |
|
|
return results |
|
|
|
|
|
@app.post("/import_graph") |
|
|
def import_graph(graph_data: GraphImportData): |
|
|
concept_store.import_from_json(graph_data.dict()) |
|
|
print(f"[INFO] Imported {len(graph_data.nodes)} nodes, {len(graph_data.edges)} edges") |
|
|
return {"status": "ok"} |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/notebook/add") |
|
|
async def add_note(req: Request): |
|
|
data = await req.json() |
|
|
text = data.get("text", "").strip() |
|
|
if not text: |
|
|
return {"status": "error", "message": "Empty text"} |
|
|
notebook.add_note(text, source="user") |
|
|
return {"status": "ok", "message": "Note added"} |
|
|
|
|
|
@app.get("/notebook/next") |
|
|
def get_next_note(): |
|
|
note = notebook.get_first_unread_note() |
|
|
if note: |
|
|
note_id, text, source, timestamp, tags = note |
|
|
return { |
|
|
"id": note_id, |
|
|
"text": text, |
|
|
"source": source, |
|
|
"timestamp": timestamp, |
|
|
"tags": tags |
|
|
} |
|
|
return {"status": "empty", "message": "No unread notes"} |
|
|
|
|
|
@app.post("/notebook/mark_read") |
|
|
async def mark_note_read(req: Request): |
|
|
data = await req.json() |
|
|
note_id = data.get("id") |
|
|
if note_id is not None: |
|
|
notebook.mark_note_as_read(note_id) |
|
|
return {"status": "ok"} |
|
|
return {"status": "error", "message": "Missing note id"} |
|
|
|
|
|
|
|
|
|
|
|
@app.route("/notes/latest", methods=["GET"]) |
|
|
def get_latest_notes(): |
|
|
"""Вернуть последние N заметок (по умолчанию 10).""" |
|
|
count = int(request.args.get("count", 10)) |
|
|
notes = storage.diary[-count:] |
|
|
return jsonify([note.to_dict() for note in notes]) |
|
|
|
|
|
@app.route("/notes/random", methods=["GET"]) |
|
|
def get_random_note(): |
|
|
"""Вернуть случайную заметку из дневника.""" |
|
|
if not storage.diary: |
|
|
return jsonify({}) |
|
|
note = random.choice(storage.diary) |
|
|
return jsonify(note.to_dict()) |
|
|
|
|
|
@app.route("/notes/set_tags", methods=["POST"]) |
|
|
def set_tags(): |
|
|
"""Обновить теги у заметки по ID.""" |
|
|
data = request.json |
|
|
note_id = data.get("id") |
|
|
tags = data.get("tags", []) |
|
|
for note in storage.diary: |
|
|
if note.id == note_id: |
|
|
note.tags = tags |
|
|
return jsonify({"status": "ok"}) |
|
|
return jsonify({"error": "not found"}), 404 |
|
|
|
|
|
@app.route("/notes/by_tag", methods=["GET"]) |
|
|
def get_notes_by_tag(): |
|
|
tag = request.args.get("tag") |
|
|
result = [note.to_dict() for note in storage.diary if tag in note.tags] |
|
|
return jsonify(result) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
uvicorn.run("mcp_server:app", host="0.0.0.0", port=8080, reload=True) |
|
|
|
|
|
|
|
|
|
|
|
@app.on_event("shutdown") |
|
|
def shutdown(): |
|
|
db.close() |
|
|
|