diff --git a/src/atocore/api/routes.py b/src/atocore/api/routes.py index e8cfc89..445ac08 100644 --- a/src/atocore/api/routes.py +++ b/src/atocore/api/routes.py @@ -32,6 +32,12 @@ from atocore.interactions.service import ( record_interaction, ) from atocore.engineering.mirror import generate_project_overview +from atocore.engineering.wiki import ( + render_entity, + render_homepage, + render_project, + render_search, +) from atocore.engineering.service import ( ENTITY_TYPES, RELATIONSHIP_TYPES, @@ -85,6 +91,33 @@ router = APIRouter() log = get_logger("api") +# --- Wiki routes (HTML, served first for clean URLs) --- + + +@router.get("/wiki", response_class=HTMLResponse) +def wiki_home() -> HTMLResponse: + return HTMLResponse(content=render_homepage()) + + +@router.get("/wiki/projects/{project_name}", response_class=HTMLResponse) +def wiki_project(project_name: str) -> HTMLResponse: + from atocore.projects.registry import resolve_project_name as _resolve + return HTMLResponse(content=render_project(_resolve(project_name))) + + +@router.get("/wiki/entities/{entity_id}", response_class=HTMLResponse) +def wiki_entity(entity_id: str) -> HTMLResponse: + html = render_entity(entity_id) + if html is None: + raise HTTPException(status_code=404, detail="Entity not found") + return HTMLResponse(content=html) + + +@router.get("/wiki/search", response_class=HTMLResponse) +def wiki_search(q: str = "") -> HTMLResponse: + return HTMLResponse(content=render_search(q)) + + # --- Request/Response models --- diff --git a/src/atocore/engineering/wiki.py b/src/atocore/engineering/wiki.py new file mode 100644 index 0000000..547b19c --- /dev/null +++ b/src/atocore/engineering/wiki.py @@ -0,0 +1,254 @@ +"""AtoCore Wiki — navigable HTML pages from structured data. + +A lightweight wiki served directly from the AtoCore API. Every page is +generated on-demand from the database so it's always current. Source of +truth is the database — the wiki is a derived view. + +Routes: + /wiki Homepage with project list + search + /wiki/projects/{name} Full project overview + /wiki/entities/{id} Entity detail with relationships + /wiki/search?q=... Search entities, memories, state +""" + +from __future__ import annotations + +import markdown as md + +from atocore.context.project_state import get_state +from atocore.engineering.service import ( + get_entities, + get_entity, + get_entity_with_context, + get_relationships, +) +from atocore.memory.service import get_memories +from atocore.projects.registry import load_project_registry + + +def render_html(title: str, body_html: str, breadcrumbs: list[tuple[str, str]] | None = None) -> str: + nav = "" + if breadcrumbs: + parts = [] + for label, href in breadcrumbs: + if href: + parts.append(f'{label}') + else: + parts.append(f"{label}") + nav = f'' + + return _TEMPLATE.replace("{{title}}", title).replace("{{nav}}", nav).replace("{{body}}", body_html) + + +def render_homepage() -> str: + projects = [] + try: + registered = load_project_registry() + for p in registered: + entity_count = len(get_entities(project=p.project_id, limit=200)) + memory_count = len(get_memories(project=p.project_id, active_only=True, limit=200)) + state_count = len(get_state(p.project_id)) + projects.append({ + "id": p.project_id, + "description": p.description, + "entities": entity_count, + "memories": memory_count, + "state": state_count, + }) + except Exception: + pass + + lines = ['
{p["description"][:120]}
') + lines.append(f'{len(all_entities)} entities · {len(all_memories)} active memories · {len(projects)} projects
') + lines.append(f'API Dashboard (JSON) · Health Check
') + + return render_html("AtoCore Wiki", "\n".join(lines)) + + +def render_project(project: str) -> str: + from atocore.engineering.mirror import generate_project_overview + + markdown_content = generate_project_overview(project) + # Convert entity names to links + entities = get_entities(project=project, limit=200) + html_body = md.markdown(markdown_content, extensions=["tables", "fenced_code"]) + + for ent in sorted(entities, key=lambda e: len(e.name), reverse=True): + linked = f'{ent.name}' + html_body = html_body.replace(f"{ent.name}", f"{linked}", 1) + + return render_html( + f"{project}", + html_body, + breadcrumbs=[("Wiki", "/wiki"), (project, "")], + ) + + +def render_entity(entity_id: str) -> str | None: + ctx = get_entity_with_context(entity_id) + if ctx is None: + return None + + ent = ctx["entity"] + lines = [f'Project: {ent.project}
') + if ent.description: + lines.append(f'{ent.description}
') + if ent.properties: + lines.append('No results found.
') + + lines.append('') + + return render_html( + f"Search: {query}", + "\n".join(lines), + breadcrumbs=[("Wiki", "/wiki"), ("Search", "")], + ) + + +_TEMPLATE = """ + + + + +