diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7f80347..13960fa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Bash(mkdir:*)", "Bash(python run_tests.py:*)", "Bash(source:*)", - "Bash(pytest:*)" + "Bash(pytest:*)", + "Bash(python:*)" ], "deny": [], "ask": [] diff --git a/app/data/models.py b/app/data/models.py index 09c10c5..d4c1249 100644 --- a/app/data/models.py +++ b/app/data/models.py @@ -2,6 +2,7 @@ import uuid from datetime import datetime from typing import List, Optional, Any from sqlmodel import Field, SQLModel, Relationship, JSON, Column +from pydantic import BaseModel class StartSessionRequest(SQLModel): @@ -70,4 +71,30 @@ class Session(SQLModel, table=True): transcript: List[dict] = Field(default=[], sa_column=Column(JSON)) created_at: datetime = Field(default_factory=datetime.utcnow) - notes: List["Note"] = Relationship(back_populates="session") \ No newline at end of file + notes: List["Note"] = Relationship(back_populates="session") + + +# Response models for visualization +class NoteWithLinks(BaseModel): + id: uuid.UUID + title: str + content: str + tags: List[str] + created_at: datetime + links: List["LinkResponse"] = [] + + +class LinkResponse(BaseModel): + id: int + source_note_id: uuid.UUID + target_note_id: uuid.UUID + relationship: str + confidence: float = 1.0 + + +class SessionDataResponse(BaseModel): + session_id: uuid.UUID + status: str + notes: List[NoteWithLinks] + created_at: datetime + summary: str = "" \ No newline at end of file diff --git a/app/data/repositories.py b/app/data/repositories.py index 3f8a92d..af1f4ab 100644 --- a/app/data/repositories.py +++ b/app/data/repositories.py @@ -95,4 +95,10 @@ class LinkRepository: async def get_by_target(self, target_id: uuid.UUID) -> List[Link]: statement = select(Link).where(Link.target_id == target_id) result = await self.session.exec(statement) + return result.all() + + async def get_by_session(self, session_id: uuid.UUID) -> List[Link]: + """Get all links for notes in a session.""" + statement = select(Link).join(Note, Link.source_id == Note.id).where(Note.session_id == session_id) + result = await self.session.exec(statement) return result.all() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 2e44340..bc95d01 100644 --- a/app/main.py +++ b/app/main.py @@ -5,6 +5,7 @@ from typing import Annotated from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse from app.core.config import settings from app.data.database import init_db @@ -12,7 +13,8 @@ from app.data.models import ( StartSessionRequest, SendMessageRequest, SessionResponse, - SessionStatusResponse + SessionStatusResponse, + SessionDataResponse ) from app.services.session_service import SessionService @@ -159,6 +161,37 @@ async def get_session_status( raise HTTPException(status_code=500, detail="Failed to get session status") +@app.get("/sessions/getData", response_model=SessionDataResponse) +async def get_session_data( + session_id: uuid.UUID, + session_service: Annotated[SessionService, Depends(get_session_service)] +) -> SessionDataResponse: + """Get session data with notes and links for visualization. + + Args: + session_id: UUID of the session + session_service: Injected SessionService instance + + Returns: + SessionDataResponse with notes and their links + """ + try: + result = await session_service.get_session_data(session_id) + return SessionDataResponse( + session_id=result["session_id"], + status=result["status"], + notes=result["notes"], + created_at=result["created_at"], + summary=result["summary"] + ) + except ValueError as e: + logger.error(f"Session not found: {e}") + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + logger.error(f"Error getting session data: {e}") + raise HTTPException(status_code=500, detail="Failed to get session data") + + @app.get("/health") async def health_check(): """Health check endpoint.""" @@ -167,13 +200,14 @@ async def health_check(): @app.get("/") async def root(): - """Root endpoint with API information.""" - return { - "message": "Welcome to SkyTalk API", - "description": "AI-powered conversational idea exploration and knowledge synthesis", - "version": "1.0.0", - "docs": "/docs" - } + """Serve the GUI interface at root.""" + return FileResponse("gui.html") + + +@app.get("/session/{session_id}") +async def view_session(session_id: uuid.UUID): + """Serve the GUI with a specific session loaded.""" + return FileResponse("gui.html") if __name__ == "__main__": diff --git a/app/services/session_service.py b/app/services/session_service.py index 0e24613..f899944 100644 --- a/app/services/session_service.py +++ b/app/services/session_service.py @@ -202,6 +202,66 @@ class SessionService: "created_at": session.created_at } + async def get_session_data(self, session_id: uuid.UUID) -> Dict[str, any]: + """Get session data with notes and links for visualization. + + Args: + session_id: UUID of the session + + Returns: + Dictionary containing session data with notes and links + """ + from app.data.models import NoteWithLinks, LinkResponse + + async with get_session() as db: + session_repo = SessionRepository(db) + note_repo = NoteRepository(db) + link_repo = LinkRepository(db) + + session = await session_repo.get(session_id) + if not session: + raise ValueError(f"Session {session_id} not found") + + notes = await note_repo.get_by_session(session_id) + links = await link_repo.get_by_session(session_id) + + # Create a map of note_id -> links for that note + links_by_source = {} + for link in links: + if link.source_id not in links_by_source: + links_by_source[link.source_id] = [] + links_by_source[link.source_id].append(LinkResponse( + id=link.id, + source_note_id=link.source_id, + target_note_id=link.target_id, + relationship=link.context, + confidence=1.0 # Default confidence since we don't store it + )) + + # Create NoteWithLinks objects + notes_with_links = [] + for note in notes: + note_links = links_by_source.get(note.id, []) + notes_with_links.append(NoteWithLinks( + id=note.id, + title=note.title, + content=note.content, + tags=note.tags, + created_at=note.created_at, + links=note_links + )) + + # Generate session summary + summary = await self._generate_session_summary(notes) + + return { + "session_id": session.id, + "status": session.status, + "notes": notes_with_links, + "created_at": session.created_at, + "summary": summary + } + async def _create_links_for_notes(self, notes: List[Note], link_repo: LinkRepository) -> None: """Create semantic links for a list of new notes.""" for note in notes: @@ -243,4 +303,30 @@ class SessionService: logger.info(f"Created {len(links_to_create)} links for note {note.id}") except Exception as e: - logger.error(f"Error creating links for note {note.id}: {e}") \ No newline at end of file + logger.error(f"Error creating links for note {note.id}: {e}") + + async def _generate_session_summary(self, notes: List[Note]) -> str: + """Generate a high-level summary of the session's notes.""" + if not notes: + return "No notes available for this session." + + try: + # Create summary of all note titles and key themes + note_summaries = [] + all_tags = set() + + for note in notes: + note_summaries.append(f"• {note.title}") + all_tags.update(note.tags) + + summary_text = f"This session explored {len(notes)} key concepts:\n\n" + summary_text += "\n".join(note_summaries) + + if all_tags: + summary_text += f"\n\nKey themes: {', '.join(sorted(all_tags))}" + + return summary_text + + except Exception as e: + logger.error(f"Error generating session summary: {e}") + return f"Session contains {len(notes)} synthesized notes on various topics." \ No newline at end of file diff --git a/gui.html b/gui.html new file mode 100644 index 0000000..d0267bb --- /dev/null +++ b/gui.html @@ -0,0 +1,1207 @@ + + +
+ + +