Aniket2006
feat: add copilot (RAG chat) endpoint alongside apk-scan and smishing
2dca8fb
Raw
History Blame Contribute Delete
2.9 kB
"""services/copilot/engine.py β€” Orchestrates retrieval β†’ prompt β†’ LLM β†’ response.
If no LLM provider is configured (or the call fails), falls back to a
deterministic "here's what I found" extractive answer built directly from
the retrieved passages β€” same graceful-degradation contract used throughout
this backend (smishing_intel's ML-vs-heuristic split, behaviour engine's
model-vs-fallback split): a missing/failing external dependency degrades
the response, it never 500s the request.
"""
from __future__ import annotations
import logging
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import Settings
from app.services.copilot import prompts
from app.services.copilot.llm.base import LLMError
from app.services.copilot.llm.factory import get_llm_provider
from app.services.copilot.models import ChatRequest, ChatResponse, ChatTurn, Citation, RetrievedChunk
from app.services.copilot.retrieval import hybrid_search
logger = logging.getLogger(__name__)
_TOP_K = 5
_SNIPPET_LENGTH = 220
def _citations_from(chunks: list[RetrievedChunk]) -> list[Citation]:
return [
Citation(
title=chunk.title,
source_path=chunk.source_path,
section=chunk.section,
snippet=(chunk.content[:_SNIPPET_LENGTH] + "…")
if len(chunk.content) > _SNIPPET_LENGTH
else chunk.content,
)
for chunk in chunks
]
def _retrieval_only_answer(chunks: list[RetrievedChunk]) -> str:
if not chunks:
return (
"I couldn't find anything in Shield's knowledge base about that. Try rephrasing, "
"or ask about a specific feature like APK scanning, threat history, or risk scores."
)
lines = ["Here's what I found (an AI provider isn't configured, so this is retrieved directly):"]
for i, chunk in enumerate(chunks, start=1):
excerpt = chunk.content[:_SNIPPET_LENGTH] + ("…" if len(chunk.content) > _SNIPPET_LENGTH else "")
lines.append(f"[{i}] {chunk.title}: {excerpt}")
return "\n\n".join(lines)
async def answer_chat(session: AsyncSession, request: ChatRequest, settings: Settings) -> ChatResponse:
chunks = await hybrid_search(session, request.message, top_k=_TOP_K)
citations = _citations_from(chunks)
try:
provider = get_llm_provider(settings)
user_prompt = prompts.build_user_prompt(request.message, request.history, chunks)
answer = await provider.generate(prompts.SYSTEM_PROMPT, user_prompt)
return ChatResponse(answer=answer.strip(), citations=citations, method="llm", provider=provider.name)
except LLMError as exc:
logger.info("LLM generation unavailable (%s) β€” returning retrieval-only answer.", exc)
return ChatResponse(
answer=_retrieval_only_answer(chunks), citations=citations, method="retrieval_only", provider=None
)