Spaces:
Sleeping
Sleeping
| """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 | |
| ) | |