| | |
| |
|
| | 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() |
| |
|