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
Empty file added repi.session.sql
Empty file.
40 changes: 1 addition & 39 deletions repi/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,10 @@
import logging
from collections import Counter
from datetime import datetime, timedelta, timezone
from typing import List, Literal, Optional
from uuid import UUID, uuid4

from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy import select, text as sa_text

from repi.core.container import get_container
Expand All @@ -44,6 +42,7 @@
from repi.llm.provider import Message
from repi.models.filters import RetrievalFilters
from repi.models.schema import ChatMessage, Conversation
from repi.api.schemas import ChatFilters, ChatRequest, ChatTurn
from repi.retrieval.cluster_view import cluster_chunks
from repi.retrieval.timeline_view import build_timeline

Expand All @@ -52,43 +51,6 @@
router = APIRouter()


# ── Request / response models ─────────────────────────────────────────────────


class ChatTurn(BaseModel):
role: Literal["user", "assistant"]
content: str


class ChatFilters(BaseModel):
service: Optional[str] = None
time_from: Optional[datetime] = None
time_to: Optional[datetime] = None
entity: Optional[str] = None


class ChatRequest(BaseModel):
query: str
history: List[ChatTurn] = []
filters: Optional[ChatFilters] = None
conversation_id: Optional[UUID] = None
# Followup-bias hint: chunk_ids the previous assistant turn cited. When
# the current query is missing EITHER an explicit service or an explicit
# time window, the chat path fills in just the missing dimension from
# the previous turn's chunks — service via dominant-source check, time
# via a `Settings.FOLLOWUP_BIAS_WINDOW_MINUTES` envelope around their
# timestamps. Soft — never overrides an explicit filter, silently
# ignored if the IDs no longer resolve.
#
# Capped at 50 to bound the indexed-PK fetch and reject malformed
# payloads early. The legitimate caller only ever sends the last
# assistant turn's citations (≤10 in practice).
previous_chunk_ids: List[str] = Field(default_factory=list, max_length=50)
# UX P1: scopes retrieval + known-services resolution to one project. If
# omitted but the conversation has a project, that project applies.
project_id: Optional[UUID] = None


# ── Module-level constants ────────────────────────────────────────────────────

# Caller-visible window on cited-chunk `text` in the SSE done payload. Locked
Expand Down
34 changes: 2 additions & 32 deletions repi/api/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,21 @@
from __future__ import annotations

import logging
from typing import List, Literal, Optional
from typing import List
from uuid import UUID

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sqlmodel import select

from repi.core.container import get_container
from repi.models.schema import ChatMessage, Conversation, Investigation, Project
from repi.api.schemas import ConversationDetail, ConversationSummary, TranscriptTurn

logger = logging.getLogger("repi.api.conversations")

router = APIRouter()


class ConversationSummary(BaseModel):
id: str
title: Optional[str]
project_id: Optional[str] = None
project_name: Optional[str] = None
created_at: str
updated_at: str


class TranscriptTurn(BaseModel):
mode: Literal["chat", "investigate"]
id: str
role: Optional[str] = None # "user" | "assistant" for chat turns
content: str
chunk_ids: List[str] = []
confidence: Optional[str] = None
status: Optional[str] = None # investigation status (chat turns leave this null)
created_at: str


class ConversationDetail(BaseModel):
id: str
title: Optional[str]
project_id: Optional[str] = None
project_name: Optional[str] = None
created_at: str
updated_at: str
turns: List[TranscriptTurn]


@router.get("/conversations", response_model=List[ConversationSummary])
async def list_conversations(limit: int = 50):
container = get_container()
Expand Down
11 changes: 1 addition & 10 deletions repi/api/ingest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import logging
from fastapi import APIRouter, UploadFile, File, Form, Depends
from pydantic import BaseModel
from repi.core.container import get_container
from repi.api.schemas import IngestResponse

logger = logging.getLogger("repi.api.ingest")

router = APIRouter()

class IngestResponse(BaseModel):
service: str
project: str
chunk_count: int
lines_total: int
lines_with_timestamp: int
level_counts: dict[str, int]
message: str

@router.post("/ingest", response_model=IngestResponse)
async def ingest(
service: str = Form(...),
Expand Down
50 changes: 7 additions & 43 deletions repi/api/investigate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,23 @@
from typing import Optional, List
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from uuid import UUID
from datetime import datetime

from repi.core.container import get_container
from repi.investigation.react_loop import InvestigationStep
from repi.api.schemas import (
ClarifyRequest,
InvestigateRequest,
InvestigationResponse,
InvestigationStepModel,
SimpleInvestigationResponse,
)

logger = logging.getLogger("repi.api.investigate")

router = APIRouter()

class InvestigateRequest(BaseModel):
query: str
resume: bool = True
# Optional thread back to a chat surface (A1/A2). If omitted, a new
# conversation row is created and its id returned so the UI can attach
# subsequent /chat turns to the same thread.
conversation_id: Optional[UUID] = None
# UX P1: scopes retrieval + every ReAct tool to one project. If omitted
# but the conversation has a project, the conversation's project applies.
project_id: Optional[UUID] = None

class InvestigationStepModel(BaseModel):
step_number: int
thought: str
# Legacy preview fields — kept for back-compat with anything that may still
# read them. The list endpoint returns empty steps anyway.
tool_name: Optional[str] = None
tool_args: Optional[dict] = None
observation_preview: Optional[str] = None
# Rich shape the UI uses to render a step identically to the SSE stream.
action: Optional[dict] = None
observation: Optional[dict] = None
kind: Optional[str] = None

class InvestigationResponse(BaseModel):
id: str
query: str
status: str
answer: Optional[str] = None
created_at: datetime
steps: List[InvestigationStepModel]
pending_question: Optional[str] = None
stats: Optional[dict] = None

class SimpleInvestigationResponse(BaseModel):
id: str
status: str
conversation_id: Optional[str] = None

class ClarifyRequest(BaseModel):
reply: str

@router.get("/investigations", response_model=List[InvestigationResponse])
async def list_investigations(limit: int = 20):
"""List recent investigations."""
Expand Down
42 changes: 1 addition & 41 deletions repi/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from uuid import UUID

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from sqlalchemy import text as sa_text
from sqlmodel import select

from repi.core.container import get_container
from repi.core.dates import DateHandler
from repi.models.schema import Project
from repi.api.schemas import ProjectCreate, ProjectRead, ProjectService, ProjectUpdate
from repi.retrieval.event_feed import derive_events, fetch_window_aggregates, parse_window

logger = logging.getLogger("repi.api.projects")
Expand Down Expand Up @@ -76,33 +76,6 @@ async def _get_or_create_by_name(session, name: str) -> Project:
return row


# ── Models ───────────────────────────────────────────────────────────────────

class ProjectCreate(BaseModel):
name: str
settings: dict[str, Any] = {}


class ProjectUpdate(BaseModel):
name: Optional[str] = None
settings: Optional[dict[str, Any]] = None


class ProjectRead(BaseModel):
id: str
name: str
settings: dict[str, Any]
service_count: int = 0
created_at: datetime
updated_at: datetime


class ProjectService(BaseModel):
name: str
chunk_count: int
last_seen: Optional[datetime] = None


# ── Endpoints ────────────────────────────────────────────────────────────────

@router.get("/projects", response_model=List[ProjectRead])
Expand Down Expand Up @@ -275,9 +248,6 @@ async def project_overview(
for r in svc_rows
]

# Suggested actions — derived, never LLM-generated. Top clusters become
# Deep-Research entry points with the service + time range pre-filled so
# the investigation starts grounded instead of from a bare phrase.
suggested: list[dict] = []
for c in clusters[:3]:
svc_part = f" on {c['services'][0]}" if c["services"] else ""
Expand All @@ -289,16 +259,6 @@ async def project_overview(
f"between {c['first_ts']} and {c['last_ts']}"
),
})
suggested.append({
"kind": "chat",
"label": f"Summarize the last {window_str}",
"query": f"summarize what happened in the last {window_str}",
})
suggested.append({
"kind": "chat",
"label": "Show affected services",
"query": "which services are having problems?",
})

return {
"project_id": str(project_id),
Expand Down
Loading
Loading