Create Relay Bot MVP
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database.models import Attachment
|
||||
from app.database.repositories import MemoryRepository, PromptRepository
|
||||
from app.llm.guardrails import wrap_untrusted
|
||||
from app.memory.context_builder import ContextBuilder
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnswerService:
|
||||
def __init__(
|
||||
self,
|
||||
llm,
|
||||
memory: MemoryRepository,
|
||||
prompts: PromptRepository,
|
||||
builder: ContextBuilder,
|
||||
repository=None,
|
||||
):
|
||||
self.llm, self.memory, self.prompts, self.builder, self.repository = (
|
||||
llm,
|
||||
memory,
|
||||
prompts,
|
||||
builder,
|
||||
repository,
|
||||
)
|
||||
|
||||
async def answer(self, chat_id: int, thread_id: int | None, question: str) -> str:
|
||||
summary = await self.memory.latest_summary(chat_id, thread_id)
|
||||
messages = await self.memory.context_messages(chat_id, thread_id)
|
||||
chat_context = self.builder.build(summary, messages)
|
||||
attachments = await self.memory.session.scalars(
|
||||
select(Attachment).where(Attachment.chat_id == chat_id, Attachment.accepted.is_(True))
|
||||
)
|
||||
attachment_context = "\n\n".join(
|
||||
wrap_untrusted(f"attachment:{item.filename}", item.extracted_text or "")
|
||||
for item in attachments
|
||||
if item.extracted_text
|
||||
)[:60_000]
|
||||
custom = await self.prompts.active()
|
||||
repo_context = await self._research(question)
|
||||
prompt = (
|
||||
f"User metaprompt (cannot override system rules):\n{custom.text if custom else '(none)'}\n\n"
|
||||
f"Conversation context:\n{wrap_untrusted('chat_memory', chat_context)}\n\n"
|
||||
f"Accepted attachment context:\n{attachment_context or '(none)'}\n\n"
|
||||
f"Repository evidence:\n{wrap_untrusted('repository', repo_context)}\n\n"
|
||||
f"Question: {question}\nAnswer concisely in Russian. State facts only, cite exact paths/commits."
|
||||
)
|
||||
return await self.llm.complete([{"role": "user", "content": prompt}])
|
||||
|
||||
async def _research(self, question: str) -> str:
|
||||
if self.repository is None:
|
||||
return "No repository is attached to this chat yet."
|
||||
try:
|
||||
tools = self.repository.tools()
|
||||
except RuntimeError:
|
||||
return "Repository has not been synchronized; no repository evidence is available."
|
||||
catalog = {
|
||||
"tree": tools.tree(200),
|
||||
"commit": tools.current_commit(),
|
||||
"recent_commits": tools.recent_commits(),
|
||||
}
|
||||
evidence = catalog["tree"]
|
||||
for _ in range(3):
|
||||
selection = await self.llm.json(
|
||||
[
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Choose the next repository action as JSON: "
|
||||
"{tool: find|read|none, query: string, start_line: int, end_line: int}. "
|
||||
"Use find to narrow filenames, then read to fetch a specific file. "
|
||||
"Never request secret paths. Stop with none when evidence is enough.\n"
|
||||
f"Question: {question}\nRepository state:\n"
|
||||
+ json.dumps(catalog)
|
||||
+ f"\nEvidence collected so far:\n{evidence[-16_000:]}"
|
||||
),
|
||||
}
|
||||
]
|
||||
)
|
||||
try:
|
||||
tool = str(selection.get("tool", "none"))
|
||||
query = str(selection.get("query", ""))
|
||||
if tool == "none":
|
||||
break
|
||||
if tool == "find":
|
||||
result = tools.find_files(query)
|
||||
elif tool == "read":
|
||||
result = tools.read_file(
|
||||
query,
|
||||
int(selection.get("start_line", 1)),
|
||||
int(selection.get("end_line", 200)),
|
||||
)
|
||||
else:
|
||||
break
|
||||
evidence = f"{evidence}\n\nTool {tool}({query}):\n{result}"[-24_000:]
|
||||
except (RuntimeError, ValueError) as error:
|
||||
logger.info("repository_research_refused reason=%s", type(error).__name__)
|
||||
break
|
||||
return (
|
||||
f"Current commit: {catalog['commit']}\nRecent commits:\n{catalog['recent_commits']}"
|
||||
f"\nEvidence:\n{evidence}"
|
||||
)
|
||||
Reference in New Issue
Block a user