Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,473 changes: 1,473 additions & 0 deletions .scratch/checkpointer-review.html

Large diffs are not rendered by default.

159 changes: 159 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<<<<<<< HEAD
# Tapio
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
Expand Down Expand Up @@ -202,3 +203,161 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
=======
# Tapio
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

Tapio is a RAG (Retrieval Augmented Generation) tool for extracting, processing, and querying information from websites like Migri.fi (Finnish Immigration Service). It provides complete workflow capabilities including web crawling, content parsing, vectorization, and an interactive chatbot interface.

## Features

- **Multi-site support** - Configurable site-specific crawling and parsing
- **End-to-end pipeline** - Crawl → Parse → Vectorize → Query workflow
- **Local LLM integration** - Uses Ollama for private, local inference
- **Semantic search** - ChromaDB vector database for relevant content retrieval
- **Interactive chatbot** - Web interface for natural language queries
- **Flexible crawling** - Configurable depth and domain restrictions
- **Comprehensive testing** - Full test suite for reliability

## Target Use Cases

**Primary Users:** EU and non-EU citizens navigating Finnish immigration processes

- Students seeking education information
- Workers exploring employment options
- Families pursuing reunification
- Refugees and asylum seekers needing guidance

**Core Needs:**

- Finding relevant, accurate information quickly
- Practice conversations on specific topics (family reunification, work permits, etc.)

## Installation and Setup

### Prerequisites

- Python 3.10 or higher
- [uv](https://github.com/astral-sh/uv) - Fast Python package installer
- [Ollama](https://ollama.com/) - For local LLM inference

### System requirements

- At least 4 GB of available RAM (the default `llama3.2` model is about 2 GB)
- For low-resource environments such as GitHub Codespaces, use a lighter model like `llama3.2:1b`

### Installing Ollama

**Linux:**

```bash
curl -fsSL https://ollama.com/install.sh | sh
```

**macOS and Windows:** Download from [ollama.com/download](https://ollama.com/download)

After installing, make sure the Ollama daemon is running before pulling models.

### Quick Start

1. Clone and setup:

```bash
git clone https://github.com/Finntegrate/tapio.git
cd tapio
uv sync
```

1. Install required Ollama model:

```bash
ollama pull llama3.2
```

For low-memory environments, use the lighter version:

```bash
ollama pull llama3.2:1b
```

## Usage

### CLI Overview

Tapio provides a four-step workflow:

1. **crawl** - Collect HTML content from websites
2. **parse** - Convert HTML to structured Markdown
3. **vectorize** - Create vector embeddings for semantic search
4. **tapio-app** - Launch the interactive chatbot interface

Use `uv run -m tapio.cli --help` to see all commands or `uv run -m tapio.cli <command> --help` for command-specific options.

### Quick Example

Complete workflow for the Migri website:

```bash
# 1. Crawl content (uses site configuration)
uv run -m tapio.cli crawl migri --depth 2

# 2. Parse HTML to Markdown
uv run -m tapio.cli parse migri

# 3. Create vector embeddings
uv run -m tapio.cli vectorize

# 4. Launch chatbot interface
uv run -m tapio.cli tapio-app
```

### Available Sites

To list configured sites:

```bash
uv run -m tapio.cli list-sites
```

To view detailed site configurations:

```bash
uv run -m tapio.cli list-sites --verbose
```

For technical details on site configurations, programmatic API usage, and adding new sites, see [CONTRIBUTING.md](CONTRIBUTING.md).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines, code style requirements, and how to submit pull requests.

## License

Licensed under the European Union Public License version 1.2. See LICENSE for details.

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brylie"><img src="https://avatars.githubusercontent.com/u/17307?v=4?s=100" width="100px;" alt="Brylie Christopher Oxley"/><br /><sub><b>Brylie Christopher Oxley</b></sub></a><br /><a href="#infra-brylie" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/finntegrate/tapio/commits?author=brylie" title="Tests">⚠️</a> <a href="https://github.com/finntegrate/tapio/commits?author=brylie" title="Documentation">📖</a> <a href="https://github.com/finntegrate/tapio/issues?q=author%3Abrylie" title="Bug reports">🐛</a> <a href="#business-brylie" title="Business development">💼</a> <a href="#content-brylie" title="Content">🖋</a> <a href="#ideas-brylie" title="Ideas, Planning, & Feedback">🤔</a> <a href="#maintenance-brylie" title="Maintenance">🚧</a> <a href="#mentoring-brylie" title="Mentoring">🧑‍🏫</a> <a href="#projectManagement-brylie" title="Project Management">📆</a> <a href="#promotion-brylie" title="Promotion">📣</a> <a href="#research-brylie" title="Research">🔬</a> <a href="https://github.com/finntegrate/tapio/pulls?q=is%3Apr+reviewed-by%3Abrylie" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/finntegrate/tapio/commits?author=brylie" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://akikurvinen.fi/"><img src="https://avatars.githubusercontent.com/u/74042688?v=4?s=100" width="100px;" alt="AkiKurvinen"/><br /><sub><b>AkiKurvinen</b></sub></a><br /><a href="#data-AkiKurvinen" title="Data">🔣</a> <a href="https://github.com/finntegrate/tapio/commits?author=AkiKurvinen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ResendeTech"><img src="https://avatars.githubusercontent.com/u/142721352?v=4?s=100" width="100px;" alt="ResendeTech"/><br /><sub><b>ResendeTech</b></sub></a><br /><a href="https://github.com/finntegrate/tapio/commits?author=ResendeTech" title="Code">💻</a></td>
</tr>
</tbody>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
>>>>>>> 97227faa11952f07fc6417898b79000034c433ec
3 changes: 1 addition & 2 deletions app/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as m from '$lib/paraglide/messages.js';
import { parseSSEStream } from './sse';
import type {
AgentSummary,
ChatMessage,
ChatStreamEvent,
CitationEventData,
ErrorEventData,
Expand All @@ -28,8 +27,8 @@ export async function getAgents(): Promise<AgentSummary[]> {

interface ChatStreamRequest {
message: string;
history: ChatMessage[];
agent_id: string;
thread_id: string;
}

/** Stream one chat turn's SSE events: routing, citation, token(s), then done or error. */
Expand Down
5 changes: 0 additions & 5 deletions app/src/lib/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
export const AUTO_ROUTE = 'auto';

export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}

export interface AgentSummary {
id: string;
name: string;
Expand Down
15 changes: 7 additions & 8 deletions app/src/lib/chat/chat-state.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as m from '$lib/paraglide/messages.js';
import { getAgents, streamChat } from '$lib/api/client';
import { AUTO_ROUTE } from '$lib/api/types';
import type { AgentSummary, ChatMessage, Citation, RoutingEventData } from '$lib/api/types';
import type { AgentSummary, Citation, RoutingEventData } from '$lib/api/types';

export interface DisplayMessage {
id: string;
Expand All @@ -13,13 +13,17 @@
isStreaming?: boolean;
}

export class ChatStore {

Check warning on line 16 in app/src/lib/chat/chat-state.svelte.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark this member as `readonly`.

See more on https://sonarcloud.io/project/issues?id=Finntegrate_tapio&issues=AaBeTq_cyxQDI1MNYc7l&open=AaBeTq_cyxQDI1MNYc7l&pullRequest=93
messages = $state<DisplayMessage[]>([]);
agents = $state<AgentSummary[]>([]);
selectedAgentId = $state<string>(AUTO_ROUTE);
isStreaming = $state(false);
error = $state<string | null>(null);

// One id per conversation, so the backend's checkpointer can recall this
// thread's history — it no longer needs to travel over the wire.
#threadId = crypto.randomUUID();

async loadAgents(): Promise<void> {
try {
this.agents = await getAgents();
Expand All @@ -32,11 +36,6 @@
const trimmed = text.trim();
if (!trimmed || this.isStreaming) return;

const history: ChatMessage[] = this.messages.map((message) => ({
role: message.role,
content: message.content
}));

this.messages.push(
{ id: crypto.randomUUID(), role: 'user', content: trimmed },
{ id: crypto.randomUUID(), role: 'assistant', content: '', isStreaming: true }
Expand All @@ -51,8 +50,8 @@
try {
for await (const event of streamChat({
message: trimmed,
history,
agent_id: this.selectedAgentId
agent_id: this.selectedAgentId,
thread_id: this.#threadId
})) {
switch (event.kind) {
case 'routing':
Expand Down
14 changes: 14 additions & 0 deletions backend/app/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Annotated

from fastapi import Depends, Request
from langgraph.graph.state import CompiledStateGraph

from app.agents.router import AgentRouter
from app.services.rag_orchestrator import RAGOrchestrator
Expand Down Expand Up @@ -32,5 +33,18 @@ def get_agent_router(request: Request) -> AgentRouter:
return request.app.state.agent_router


def get_graph(request: Request) -> CompiledStateGraph:
"""Return the memory-backed conversation graph built during app startup.

Args:
request: The current request, used to reach ``app.state``.

Returns:
The shared, checkpointer-backed conversation graph.
"""
return request.app.state.graph


OrchestratorDep = Annotated[RAGOrchestrator, Depends(get_orchestrator)]
AgentRouterDep = Annotated[AgentRouter, Depends(get_agent_router)]
GraphDep = Annotated[CompiledStateGraph, Depends(get_graph)]
8 changes: 6 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from app.config import BackendSettings
from app.config.config_models import RAGConfig
from app.factories import RAGOrchestratorFactory
from app.memory.checkpointer import get_checkpointer
from app.memory.graph import build_graph
from app.routes import agents, chat, health

logger = logging.getLogger(__name__)
Expand All @@ -29,8 +31,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""
app.state.orchestrator = RAGOrchestratorFactory(RAGConfig()).create_orchestrator()
app.state.agent_router = AgentRouter()
logger.info("Tapio backend started")
yield
async with get_checkpointer() as checkpointer:
app.state.graph = build_graph(checkpointer, app.state.orchestrator, app.state.agent_router)
logger.info("Tapio backend started")
yield


def create_app() -> FastAPI:
Expand Down
1 change: 1 addition & 0 deletions backend/app/memory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Conversation memory: LangGraph checkpointer and graph wrapper for RAGOrchestrator."""
28 changes: 28 additions & 0 deletions backend/app/memory/checkpointer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Checkpointer for conversation memory."""

import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver


@asynccontextmanager
async def get_checkpointer() -> AsyncIterator[BaseCheckpointSaver]:
"""Yield a checkpointer, held open for the caller's lifetime.

Postgres by default. Set USE_MEMORY_SAVER=1 for an in-memory store that is
wiped on restart — local development only, and not safe across workers.

Yields:
A checkpointer ready to pass to build_graph
"""
if os.getenv("USE_MEMORY_SAVER") == "1":
yield MemorySaver()
return

async with AsyncPostgresSaver.from_conn_string(os.environ["DATABASE_URL"]) as saver:
await saver.setup()
yield saver
Loading
Loading