Skip to content
Open
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
46 changes: 23 additions & 23 deletions .kilo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ GROQ_API_KEY=your_groq_api_key
### 3. Run Backend

```powershell
python -m uvicorn backend.main:app --reload
python -m uvicorn src.main:app --reload
```

Backend runs at http://localhost:8000
Expand Down Expand Up @@ -81,4 +81,4 @@ pytest tests/ -v

## Architecture

See `architecture.md` for detailed documentation.
See `architecture.md` for detailed documentation.
37 changes: 32 additions & 5 deletions src/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from typing import Optional

from .deps import get_db_session, get_current_user
from ..db.repositories import UserRepository, UserSettingRepository
from ..db.models import User, UserSetting
from ..infrastructure.database.repositories import UserRepository, UserSettingRepository
from ..domain.models import User, UserSetting
from ..shared.security import hash_password, verify_password, create_access_token

router = APIRouter(prefix="/auth", tags=["authentication"])
Expand Down Expand Up @@ -34,8 +34,8 @@ class TokenResponse(BaseModel):
token: str


@router.post("/signup", response_model=TokenResponse)
def signup(req: SignupRequest, db: Session = Depends(get_db_session)):
@router.post("/register", response_model=TokenResponse)
def register(req: SignupRequest, db: Session = Depends(get_db_session)):
repo = UserRepository(User, db)

if repo.get_by_username(req.username):
Expand Down Expand Up @@ -78,4 +78,31 @@ def get_me(current_user: User = Depends(get_current_user)):
user_id=current_user.id,
username=current_user.username,
email=current_user.email
)
)


class SettingUpdate(BaseModel):
key: str
value: str


@router.get("/settings")
def get_settings_endpoint(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db_session)
):
repo = UserSettingRepository(UserSetting, db)
settings = repo.get_all_for_user(current_user.id)
return {s.key: s.value for s in settings}


@router.post("/settings")
def update_settings(
req: SettingUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db_session)
):
repo = UserSettingRepository(UserSetting, db)
repo.upsert(current_user.id, req.key, req.value)
db.commit()
return {"status": "ok"}
103 changes: 36 additions & 67 deletions src/api/chat.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List

from .deps import get_db_session, get_current_user, get_user_settings
from ..db.repositories import ChatSessionRepository, MessageRepository, UserSettingRepository, KnowledgeBaseRepository
from ..db.models import ChatSession, Message, User, UserSetting, KnowledgeBase
from ..services.llm_factory import LLMFactory
try:
from ..services.web_search_service import WebSearchService
except (ImportError, ModuleNotFoundError):
try:
from src.services.web_search_service import WebSearchService
except (ImportError, ModuleNotFoundError):
from services.web_search_service import WebSearchService
from .deps import get_db_session, get_current_user
from ..infrastructure.database.repositories import ChatSessionRepository, MessageRepository, UserSettingRepository, KnowledgeBaseRepository
from ..domain.models import ChatSession, Message, User, UserSetting, KnowledgeBase

from ..core.settings import get_settings

router = APIRouter(prefix="/api", tags=["chat"])

@router.get("/llm-provider")
def get_llm_provider(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db_session)
):
settings = get_settings()

# Check user-specific Groq key
settings_repo = UserSettingRepository(UserSetting, db)
user_key = settings_repo.get_by_user_and_key(current_user.id, "groq_api_key")

has_groq = bool(user_key and user_key.value) or bool(settings.groq_api_key)

return {
"active_provider": "groq" if has_groq else "ollama",
"groq": {"available": has_groq},
"ollama": {
"available": True, # Assume local ollama is available if Groq is not
"model": settings.ollama_model
}
}


class CreateSessionRequest(BaseModel):
kb_id: Optional[int] = None
Expand Down Expand Up @@ -135,31 +151,11 @@ def get_session_messages(

@router.post("/chat", response_model=ChatResponse)
def chat(
request: Request,
req: ChatRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db_session)
):
from ..core.langgraph.nodes import RAGOrchestrator

# Get API key (user-level or system-level)
settings_repo = UserSettingRepository(UserSetting, db)
api_key_setting = settings_repo.get_by_user_and_key(current_user.id, "groq_api_key")
api_key = api_key_setting.value if api_key_setting else None

if not api_key:
from ..shared.config import get_settings
api_key = get_settings().groq_api_key

# Use LLM Factory — auto-detects Groq or Ollama
try:
llm_service = LLMFactory.create(api_key=api_key)
except Exception as factory_err:
raise HTTPException(
400,
"No LLM provider available. Either add a GROQ API key in Settings "
"or install Ollama (https://ollama.com) for free local LLM."
)

# Get or create session
session_repo = ChatSessionRepository(ChatSession, db)
if req.session_id:
Expand All @@ -176,52 +172,25 @@ def chat(

# Save user message
msg_repo = MessageRepository(Message, db)
user_msg = msg_repo.create(
msg_repo.create(
session_id=session.id,
role="user",
content=req.message
)
db.flush()

# Perform RAG chat
# Perform RAG chat using the injected service
try:
orchestrator = RAGOrchestrator(llm_service)

context = {}

# Support both kb_id (legacy) and kb_ids (new)
kb_ids_to_search = []
if req.kb_ids:
kb_ids_to_search.extend(req.kb_ids)
elif req.kb_id:
kb_ids_to_search.append(req.kb_id)

if kb_ids_to_search:
kb_repo = KnowledgeBaseRepository(KnowledgeBase, db)
valid_kbs = []
for k_id in kb_ids_to_search:
if kb_repo.get_by_user_and_id(k_id, current_user.id):
valid_kbs.append(k_id)
if valid_kbs:
context["kb_ids"] = valid_kbs

if req.enable_web_search:
web_search = WebSearchService()
search_results = web_search.search(req.message, max_results=3)
context["web_results"] = [
{"title": r.title, "body": r.body, "href": r.href}
for r in search_results
]

result = orchestrator.chat(req.message, context=context)
rag_service = request.app.state.rag_service
result = rag_service.answer(req.message)

# Save assistant message
assistant_msg = msg_repo.create(
msg_repo.create(
session_id=session.id,
role="assistant",
content=result.get("response", ""),
intent=result.get("intent"),
confidence=result.get("confidence"),
confidence=str(result.get("confidence")),
sources=result.get("sources")
)
db.commit()
Expand All @@ -230,7 +199,7 @@ def chat(
response=result.get("response", ""),
session_id=session.id,
intent=result.get("intent"),
confidence=result.get("confidence"),
confidence=str(result.get("confidence")),
sources=result.get("sources")
)

Expand Down
10 changes: 5 additions & 5 deletions src/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
from sqlalchemy.orm import Session
from typing import Optional

from ..db.database import Database
from ..db.repositories import UserRepository, UserSettingRepository
from ..db.models import User, UserSetting
from ..infrastructure.database.database import Database
from ..infrastructure.database.repositories import UserRepository, UserSettingRepository
from ..domain.models import User, UserSetting
from ..shared.security import decode_access_token
from ..shared.config import get_settings
from ..core.settings import get_settings


def get_database() -> Database:
return Database(get_settings().database_url)
return Database(get_settings().db_url)


def get_db_session(db: Database = Depends(get_database)) -> Session:
Expand Down
12 changes: 6 additions & 6 deletions src/api/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

try:
from .deps import get_db_session, get_current_user
from ..db.repositories import DocumentRepository, KnowledgeBaseRepository, UserSettingRepository
from ..db.models import Document, User, UserSetting, KnowledgeBase
from ..infrastructure.database.repositories import DocumentRepository, KnowledgeBaseRepository, UserSettingRepository
from ..domain.models import Document, User, UserSetting, KnowledgeBase
from ..shared.exceptions import NotFoundError, ValidationError
except (ImportError, ModuleNotFoundError):
try:
from src.api.deps import get_db_session, get_current_user
from src.db.repositories import DocumentRepository, KnowledgeBaseRepository, UserSettingRepository
from src.db.models import Document, User, UserSetting, KnowledgeBase
from src.infrastructure.database.repositories import DocumentRepository, KnowledgeBaseRepository, UserSettingRepository
from src.domain.models import Document, User, UserSetting, KnowledgeBase
from src.shared.exceptions import NotFoundError, ValidationError
except (ImportError, ModuleNotFoundError):
# Fallback if src is root
Expand Down Expand Up @@ -152,7 +152,7 @@ async def upload_document(

def _process_document_task(doc_id: int, kb_id: int, file_path_str: str, filename: str, db_url: str):
"""Background task: extract text, then index document, then update DB status."""
from ..db.database import Database
from ..infrastructure.database.database import Database
from ..core.search.dynamic_index import IndexManager
import asyncio

Expand Down Expand Up @@ -318,7 +318,7 @@ def summarize(

def cleanup_stuck_documents(db: Session):
"""Mark documents stuck in 'processing' as 'failed' or ready to resume."""
from ..db.models import Document
from ..domain.models import Document
stuck_docs = db.query(Document).filter(Document.index_status == "processing").all()
for doc in stuck_docs:
print(f"SYSTEM: Found stuck document {doc.id} ({doc.title}). Resetting to 'failed'.")
Expand Down
4 changes: 2 additions & 2 deletions src/api/knowledge_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from typing import Optional, List

from .deps import get_db_session, get_current_user, get_pagination_params
from ..db.repositories import KnowledgeBaseRepository, DocumentRepository
from ..db.models import KnowledgeBase, Document, User
from ..infrastructure.database.repositories import KnowledgeBaseRepository, DocumentRepository
from ..domain.models import KnowledgeBase, Document, User
from ..shared.exceptions import NotFoundError
import shutil
from pathlib import Path
Expand Down
Loading
Loading