Skip to content

akshayrpatel/edith

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

1 Commit
ย 
ย 
ย 
ย 

Repository files navigation

E.D.I.T.H.

E.D.I.T.H.

A personal multi-agent orchestration system designed to automate and augment my workflows.

ย 

๐Ÿง  Mission Briefing

E.D.I.T.H. is a full-stack application comprising a stateless FastAPI backend and a Next.js conversational frontend. Each task is handled by a dedicated LangGraph agent - an independent, compiled state machine with its own pipeline, prompt architecture, and output renderer.

A centralized service registry wires agents at startup, injects shared infrastructure (LLM providers, configuration), and exposes them through a unified API with real-time SSE progress streaming. The frontend consumes these streams, rendering agent progress in real time and presenting generated artifacts like PDFs inline.

The current agent fleet handles career document automation - transforming raw job descriptions into tailored cover letters and resumes as print-ready PDFs. The architecture is extensible: new agents plug into the same registry, router, and LLM infrastructure without touching existing code.

ย 


Backend

ย 

๐Ÿค– The Agent Fleet

Career Agents

01 ย  CoverLetterAgentService - Tailored Cover Letter Generation

Contract: Raw job description in โ†’ personalized, print-ready cover letter PDF out.

A 3-node LangGraph state machine that extracts company name and position title via structured LLM output + regex parsing, generates 2-3 constrained body paragraphs using the candidate profile and a style-reference template (using only verifiable facts), and renders the final document by converting markdown to HTML and producing a PDF via WeasyPrint.

ย 

02 ย  ResumeAgentService - Config-Driven Resume Tailoring

Contract: Raw job description in โ†’ fully tailored resume PDF out (rewritten summary, per-employer bullets, reordered skills).

The most complex agent in the fleet. Its tailor_content node uses a dynamically assembled prompt built at module import time from YAML configuration. The LLM produces multi-section output delimited by ===SECTION=== markers, which a regex parser splits into structured HTML fragments. Adding a new employer section or skill category requires only a YAML edit - the prompt builder, section parser, and PDF renderer all adapt automatically.

ย 

Orchestration Core

01 ย  LLMService - Multi-Provider Intelligence Layer

Contract: chat(messages) -> str ย |ย  stream(messages) -> AsyncGenerator[str]

Abstracts all LLM provider logic behind a unified async interface. Maintains an ordered provider chain (Ollama โ†’ Mistral โ†’ Groq โ†’ OpenRouter) with automatic sequential failover. If local inference is unavailable, the request transparently falls through to cloud providers. On complete exhaustion, both methods return a graceful degradation message - no raw exceptions ever surface to callers.

ย 

02 ย  ServiceRegistry - Lifecycle & Dependency Injection

A module-level singleton that holds typed references to every service. Services are wired with explicit constructor injection - the LLMService instance is passed into each agent's __init__, not imported globally. This de-coupling means agents can be tested in isolation by injecting a mock provider.

ย 

03 ย  Router - Unified API Gateway & SSE Dispatcher

Routes incoming requests to the correct agent based on agent_id, formats SSE events, and serves generated PDFs. The /edith/api/chat endpoint acts as the central dispatch - when no agent_id is provided, the request falls through to a direct LLM token stream.

ย 

04 ย  PDF Renderers - Template-to-Document Engines

Contract: render(placeholders, output_path) -> None

Each renderer loads its HTML template once at initialization (cached as a string) and performs string replacement for each placeholder at render time. WeasyPrint handles HTML + CSS โ†’ PDF conversion - no Jinja2, no template engine dependency.

ย 

โšก Execution Flow

Client
  โ”‚
  โ”‚  POST /edith/api/cover-letter/stream
  โ”‚  {query: "job description...", session_id: "..."}
  โ”‚
  โ–ผ
Router โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  โ”‚  Resolve agent from ServiceRegistry
  โ”‚  Open SSE stream
  โ”‚
  โ–ผ
Agent Pipeline (CoverLetterAgentService)
  โ”‚
  โ”œโ”€ [1] extract_details โ”€โ”€โ–ถ LLM โ”€โ”€โ–ถ Regex Parse
  โ”‚       โ—€โ”€โ”€ SSE: stage "analyzing"
  โ”‚       โ—€โ”€โ”€ SSE: stage "details_extracted"
  โ”‚
  โ”œโ”€ [2] generate_body โ”€โ”€โ–ถ LLM (profile + JD + style ref)
  โ”‚       โ—€โ”€โ”€ SSE: stage "generating"
  โ”‚
  โ”œโ”€ [3] render_pdf โ”€โ”€โ–ถ markdown โ†’ HTML โ†’ WeasyPrint โ†’ PDF
  โ”‚       โ—€โ”€โ”€ SSE: stage "rendering"
  โ”‚
  โ–ผ
  โ—€โ”€โ”€ SSE: message  (human-readable summary)
  โ—€โ”€โ”€ SSE: complete (pdf_download_url, session_id)

Each agent exposes two execution paths for the same pipeline logic:

  • handle() - runs the compiled graph via ainvoke for synchronous JSON responses
  • handle_stream() - manually sequences node functions with interleaved SSE events for real-time progress

The dual-path design exists because LangGraph's ainvoke doesn't support injecting events between node executions. The streaming path calls each node function manually, merges state immutably, and yields SSE events between steps.

ย 

๐Ÿ—๏ธ Architecture & Design Decisions

  1. Multi-agent over a single prompt - A monolithic prompt that handles extraction, generation, and rendering would stuff the entire context into a single LLM call. With discrete agents, each node validates its output before passing state forward, context windows stay focused, and every new agent inherits the LLM failover chain, SSE streaming, PDF rendering, and service lifecycle for free. Modularity over monolith - always.

  2. Service registry with constructor injection - All services are instantiated through the service registry during FastAPI's async lifespan startup. Deterministic initialization order, clean shutdown, and testability through __init__ parameters rather than global imports. This is Inversion of Control - agents don't know where their LLMService comes from.

  3. Config-driven prompt assembly - The resume agent's prompt is not a static string. A prompt builder runs once at module import time, iterating over employer configs and skill categories from YAML. Adding an employer, changing tailor instructions, or adding skill categories - all YAML edits, zero code changes. The prompt structure is code; the prompt content is configuration.

  4. Regex-based structured extraction - Both agents parse LLM output with compiled regex patterns rather than asking the LLM to produce JSON. Regex is deterministic - it never produces malformed structure. The failure mode is a missing match (handled with safe defaults) rather than a corrupted parse that crashes the pipeline.

  5. SSE over WebSockets - The progress stream is unidirectional (server โ†’ client). SSE requires no connection upgrade negotiation, no heartbeat management, no bidirectional state. A simpler protocol for a simpler contract.

  6. str.replace() over Jinja2 - Templates have 3-6 fixed placeholder markers. A full template engine adds a dependency and import overhead for a problem that string replacement handles in microseconds. Intentional minimalism.

  7. Dual execution paths per agent - handle() runs the compiled graph for idempotent JSON responses. handle_stream() bypasses the graph entirely, manually calling each node to interleave SSE events. Same logic, different delivery mechanisms - necessary because LangGraph doesn't support mid-pipeline event injection.

  8. WeasyPrint over ReportLab - Documents are authored in HTML/CSS - a familiar, maintainable format. WeasyPrint handles CSS paged media, fonts, and layout. No programmatic PDF primitives, no coordinate-based positioning. The web platform is the document authoring layer.

ย 

๐Ÿ› ๏ธ Backend Tech Stack

Technology Role in E.D.I.T.H.
Python 3.12+ Strictly typed runtime - 100% type coverage across all modules
FastAPI Async HTTP framework with lifespan management, CORS, and exception handling
LangGraph Agent orchestration - StateGraph compiled into immutable CompiledStateGraph
LangChain Core LLM abstraction - BaseChatModel, ainvoke, astream across providers
Ollama Primary LLM provider - local inference, zero API cost
Mistral / Groq / OpenRouter Cloud LLM fallback providers - activatable for resilience
Pydantic v2 Strict-mode validation for all DTOs, settings, and SSE payloads
WeasyPrint CSS paged-media PDF engine - documents authored in HTML/CSS
SSE Real-time unidirectional progress streaming via StreamingResponse
uv Dependency management and virtual environment tooling

ย 

๐Ÿ† Backend Engineering Wins

  1. Multi-provider failover with async streaming - The LLM service iterates providers sequentially. On success, the generator yields all chunks and returns (ensuring only one provider streams per call). On failure, the exception is caught, logged, and execution continues to the next. A triple-guard on each chunk (isinstance checks + empty string filter) prevents malformed frames from hitting the SSE layer. If a provider fails mid-stream, the next restarts from the beginning - acceptable for stateless generation, avoids checkpoint complexity.

  2. Import-time prompt compilation - The resume agent's tailor prompt is a module-level constant, not computed per-request. A prompt builder reads employer configs and skill categories from YAML at import time and assembles the template once. At request time, only the job description and profile are injected. Zero per-request string assembly overhead.

  3. Functional state immutability - The streaming methods don't mutate state in place. Each node's output is merged via dict spread, producing a new dict on every step. Explicit, traceable state transitions - useful for debugging multi-node pipelines where intermediate state matters.

  4. Graceful degradation over hard failure - On exhaustion of all LLM providers, both chat() and stream() return a user-friendly fallback string rather than raising an exception. The user sees a polite message; the pipeline never crashes from a provider outage. Downstream agents receive a string response in all cases - the contract holds regardless of infrastructure state.

  5. Compiled graph reuse - Each agent's StateGraph is compiled once at initialization into an immutable CompiledStateGraph. This compiled object is reused across all concurrent requests - thread-safe, zero per-request graph construction overhead.

ย 


Frontend

E.D.I.T.H. Landing

ย 

๐Ÿง  Overview

E.D.I.T.H. UI is a light-themed chat interface built with Next.js 15 and Tailwind CSS v4. It connects to the E.D.I.T.H. backend, consuming its SSE streams, rendering agent progress in real time, and presenting generated artifacts like PDFs and calendar events inline.

The interface follows a single-page conversational layout inspired by Google Gemini: a centered landing with health status, a sticky input bar with agent selection, and a scrolling chat window with markdown rendering and rich agent-specific cards.

ย 

โœจ Features

  • Real-time SSE streaming - assistant responses stream token-by-token with a live typing indicator
  • Agent picker - select a specialized agent (cover letter, resume, calendar) before sending a message, or default to general chat
  • Rich artifact cards - agent outputs render as interactive cards with PDF download links, calendar event details, and action buttons
  • Markdown rendering - full markdown support in assistant responses
  • Health-aware UI - the landing page reflects backend connectivity state (connecting / online / offline) with randomized welcome messages and status text
  • Responsive layout - mobile-friendly, max-width contained, sticky input bar

ย 

โšก Frontend Architecture

The UI is a single-page client application with a straightforward data flow:

User Input
  โ”‚
  โ”œโ”€ No agent selected โ”€โ”€โ–ถ POST /edith/api/chat โ”€โ”€โ–ถ Direct LLM token stream
  โ”‚
  โ”œโ”€ Agent selected โ”€โ”€โ–ถ POST /edith/api/{agent}/stream โ”€โ”€โ–ถ SSE progress events
  โ”‚                                                         โ”œโ”€ stage updates
  โ”‚                                                         โ”œโ”€ streamed tokens
  โ”‚                                                         โ””โ”€ completion + artifacts
  โ”‚
  โ–ผ
Chat Window
  โ”œโ”€ Markdown messages (general chat)
  โ””โ”€ Rich artifact cards (agent responses)

A single useChat hook manages all conversation state - message history, loading state, and SSE stream consumption. The hook parses incoming SSE events, appends streamed tokens to the current assistant message, and extracts structured metadata (PDF URLs, calendar events) into typed artifact objects rendered by dedicated card components.

ย 

๐Ÿ› ๏ธ Frontend Tech Stack

Technology Role
Next.js 15 App Router, React Server Components, Turbopack dev server
React 19 UI rendering with the React Compiler enabled
TypeScript 5 Strict typing across all components, hooks, and types
Tailwind CSS v4 Utility-first styling - light theme only, no dark mode
animate.css Entry animations for landing elements and transitions
react-markdown Markdown-to-JSX rendering for assistant messages
Bun Package management and script runner

ย 

๐Ÿ† Frontend Engineering Wins

  1. Manual SSE parser with buffered line splitting - The SSE helper doesn't rely on EventSource (which only supports GET). Instead, it POSTs with a JSON body, reads the response as a ReadableStream, and implements the SSE wire protocol manually - buffering partial chunks, splitting on newlines, matching event: and data: prefixes, and flushing complete frames. The parser handles chunk boundaries that split mid-line, carriage return cleanup, and malformed frames (silently skipped). All of this to support POST-based SSE - a pattern the browser's native EventSource API simply cannot do.

  2. Dual streaming paths in a single hook - useChat handles two fundamentally different stream formats through one interface. Plain chat uses raw text streaming, while agent endpoints use structured SSE with typed event dispatch (stage, message, complete, error). The consumer component doesn't know or care which path was taken - it receives the same message shape regardless. One hook, two protocols, zero leaking abstractions.

  3. Optimistic message insertion with in-place mutation - When the user sends a message, both the user message and an empty assistant placeholder are appended synchronously before the network request fires. As SSE events arrive, the placeholder is updated in-place - matching by a pre-generated UUID. No message reordering, no loading-then-swap flicker. The conversation feels instant even on slow connections.

  4. Auto-resizing textarea with frame-perfect measurement - The input textarea grows with content up to a 200px cap. Height recalculation uses requestAnimationFrame to defer measurement until after React's DOM flush, avoiding the stale-height flash that happens when measuring synchronously. External value changes (like clearing after submit) also trigger remeasurement.

  5. Randomized personality at session scope - Welcome messages, status text, and input placeholders are drawn from curated arrays using random indices generated once via useState initializer. The indices are stable for the session (no re-rolls on re-render), so the personality is consistent within a visit but varied across visits. Three independent index pools ensure the welcome, status, and placeholder don't always correlate.

ย 

๐Ÿ—๏ธ Frontend Design Decisions

  1. SSE consumption in a custom hook - All streaming logic lives in one hook rather than scattered across components. Components receive messages as props and remain purely presentational.

  2. Agent-specific card components - Each agent type has a dedicated card renderer. This keeps rendering logic isolated and makes adding new agent UIs a matter of adding a new card component.

  3. Health check on mount - The app pings the backend on load and reflects the result immediately. When offline, the input is disabled and the landing communicates the disconnected state - no silent failures.

  4. Light theme only - A deliberate constraint matching the Gemini design language. No theme toggle, no dark mode CSS - reduces complexity and keeps the visual identity consistent.

  5. Randomized copy - Welcome messages, status text, and input placeholders are randomly selected per session from curated arrays. Small detail that makes the UI feel less mechanical.

ย 


๐Ÿ”ฎ Future Roadmap

E.D.I.T.H. is designed as a personal agent fleet, not a single-purpose tool. The architecture - service registry, LLM failover, SSE streaming, config-driven prompts - is agent-agnostic. Planned additions:

  • Health Agent - tracking and analyzing personal health metrics
  • Recipe Agent - meal planning based on dietary goals and ingredient availability
  • Grocery Agent - automated shopping list generation from meal plans
  • Portfolio Agent - RAG-based assistant for interactive portfolio Q&A (migrating from Jarvis)

Each new agent plugs into the existing registry, router, and LLM infrastructure without modifying existing code.

ย 

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors