From 3f18ba3b35541dd42e87e9dd7d89187650623b78 Mon Sep 17 00:00:00 2001 From: Anto01 Date: Mon, 13 Apr 2026 16:09:12 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20AtoCore=20Wiki=20=E2=80=94=20navigable?= =?UTF-8?q?=20project=20knowledge=20browser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full wiki interface at /wiki with: - /wiki — Homepage with project cards, search box, system stats - /wiki/projects/{name} — Project page with clickable entity links - /wiki/entities/{id} — Entity detail with relationships as links - /wiki/search?q=... — Search across entities and memories Every entity name in a project page links to its detail page. Entity detail pages show properties, relationships as clickable links to related entities, and breadcrumb navigation back to the project and wiki home. Responsive, dark-mode, mobile-friendly. Card grid for projects. Generated on-demand from the database — always current, no static files, source of truth is the DB. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/atocore/api/routes.py | 33 +++++ src/atocore/engineering/wiki.py | 254 ++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+) create mode 100644 src/atocore/engineering/wiki.py 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 = ['

AtoCore Wiki

'] + lines.append('') + + lines.append('

Projects

') + lines.append('') + + # Quick stats + all_entities = get_entities(limit=500) + all_memories = get_memories(active_only=True, limit=500) + lines.append('

System

') + 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'

[{ent.entity_type}] {ent.name}

'] + + if ent.project: + lines.append(f'

Project: {ent.project}

') + if ent.description: + lines.append(f'

{ent.description}

') + if ent.properties: + lines.append('

Properties

') + + lines.append(f'

confidence: {ent.confidence} · status: {ent.status} · created: {ent.created_at}

') + + if ctx["relationships"]: + lines.append('

Relationships

') + + breadcrumbs = [("Wiki", "/wiki")] + if ent.project: + breadcrumbs.append((ent.project, f"/wiki/projects/{ent.project}")) + breadcrumbs.append((ent.name, "")) + + return render_html(ent.name, "\n".join(lines), breadcrumbs=breadcrumbs) + + +def render_search(query: str) -> str: + lines = [f'

Search: "{query}"

'] + + # Search entities by name + entities = get_entities(name_contains=query, limit=20) + if entities: + lines.append(f'

Entities ({len(entities)})

') + + # Search memories + all_memories = get_memories(active_only=True, limit=200) + query_lower = query.lower() + matching_mems = [m for m in all_memories if query_lower in m.content.lower()][:10] + if matching_mems: + lines.append(f'

Memories ({len(matching_mems)})

') + + if not entities and not matching_mems: + lines.append('

No results found.

') + + lines.append('') + + return render_html( + f"Search: {query}", + "\n".join(lines), + breadcrumbs=[("Wiki", "/wiki"), ("Search", "")], + ) + + +_TEMPLATE = """ + + + + +{{title}} — AtoCore + + + +{{nav}} +{{body}} + +"""