Multimodel_Rag / app /agent /tools.py
Dhrumil Parikh
deploy GeminiRAG
cdc55f4
Raw
History Blame Contribute Delete
9.46 kB
"""
Tool implementations used by the Groq agent in agent.py.
All tools are plain Python functions called directly by the agent (not via ADK
MCP decorators in the final implementation). Each returns a JSON-serialisable
dict or str.
Tools
-----
set_agent_user_id(user_id) — stores the current user's UUID in a
ContextVar so tools can scope DB queries
without passing user_id through every call.
ingest_file(file_path) — copies a file into UPLOAD_DIR, creates a
Job row, and enqueues process_file.
get_job_status(job_id) — polls Job.status / step / error from the DB.
query_rag(question, job_ids) — runs the full RAG engine and returns the
answer with citations.
list_documents() — returns completed jobs with chunk counts and
the total embedded vector count from ChromaDB.
summarize_document(job_id) — returns the structured summary JSON stored
in Job.result by the processor.
"""
import json
import shutil
import time
import uuid
from contextvars import ContextVar
from pathlib import Path
from sqlmodel import Session, select
from app.models.db import Job, JobStatus, get_engine
from app.observability.logging import get_logger
log = get_logger()
_current_user_id: ContextVar[str | None] = ContextVar("_current_user_id", default=None)
def set_agent_user_id(user_id: str):
return _current_user_id.set(user_id)
def ingest_file(file_path: str) -> dict:
"""
Submit a file for background processing.
Args:
file_path: Absolute path to the file to process.
Returns:
job_id: UUID of the created job.
status: Initial status (always PENDING).
message: Confirmation message.
"""
start = time.monotonic()
try:
from app.config import settings
from app.workers.tasks import process_file
user_id_str = _current_user_id.get()
if not user_id_str:
return {"error": "No authenticated user context"}
path = Path(file_path)
if not path.exists():
return {"error": f"File not found: {file_path}"}
suffix = path.suffix.lower().lstrip(".")
file_type_map = {
"pdf": "pdf", "docx": "docx", "xlsx": "xlsx", "csv": "csv",
"png": "image", "jpg": "image", "jpeg": "image",
"gif": "image", "webp": "image",
"mp4": "video", "avi": "video", "mov": "video",
"mp3": "audio", "wav": "audio", "m4a": "audio",
}
file_type = file_type_map.get(suffix)
if not file_type:
return {"error": f"Unsupported file extension: .{suffix}"}
upload_dir = Path(settings.UPLOAD_DIR)
upload_dir.mkdir(parents=True, exist_ok=True)
dest = upload_dir / path.name
shutil.copy2(file_path, dest)
with Session(get_engine()) as db:
job = Job(
user_id=uuid.UUID(user_id_str),
filename=path.name,
file_type=file_type,
file_path=str(dest),
file_size_bytes=path.stat().st_size,
status=JobStatus.pending,
)
db.add(job)
db.commit()
db.refresh(job)
job_id = str(job.id)
process_file.delay(job_id)
result = {"job_id": job_id, "status": "PENDING", "message": f"File '{path.name}' submitted for processing."}
except Exception as exc:
result = {"error": str(exc)}
latency_ms = int((time.monotonic() - start) * 1000)
log.info("tool_call", tool_name="ingest_file", latency_ms=latency_ms, result_preview=str(result)[:200])
return result
def get_job_status(job_id: str) -> dict:
"""
Check the current processing status of a job.
Args:
job_id: UUID of the job to check.
Returns:
status: PENDING, PROCESSING, COMPLETED, FAILED, or FAILED_PERMANENT.
step: Current processing step (e.g. 'embedding', 'indexing').
retry_count: Number of retries so far.
error_message: Error details if failed, null otherwise.
"""
start = time.monotonic()
try:
try:
jid = uuid.UUID(job_id)
except ValueError:
return {"error": f"Invalid job_id: {job_id}"}
with Session(get_engine()) as db:
job = db.get(Job, jid)
if not job:
result = {"error": f"Job not found: {job_id}"}
else:
result = {
"job_id": str(job.id),
"status": job.status.value,
"step": job.step,
"retry_count": job.retry_count,
"chunk_count": job.chunk_count,
"error_message": job.error_message,
}
except Exception as exc:
result = {"error": str(exc)}
latency_ms = int((time.monotonic() - start) * 1000)
log.info("tool_call", tool_name="get_job_status", latency_ms=latency_ms, result_preview=str(result)[:200])
return result
def query_rag(question: str, job_ids: list[str] | None = None) -> dict:
"""
Answer a question using the RAG engine across processed documents.
Args:
question: Natural language question to answer.
job_ids: Optional list of job UUIDs to restrict search to. If null, searches all documents.
Returns:
answer: Grounded answer with [n] citation markers.
citations: List of source references.
confidence_gate_passed: Whether enough relevant context was found.
"""
start = time.monotonic()
try:
from app.config import settings
from app.rag import engine
user_id_str = _current_user_id.get()
if not user_id_str:
return {"error": "No authenticated user context"}
with Session(get_engine()) as db:
result = engine.query(
question=question,
job_ids=job_ids,
user_id=uuid.UUID(user_id_str),
db=db,
settings=settings,
)
except Exception as exc:
result = {"error": str(exc)}
latency_ms = int((time.monotonic() - start) * 1000)
log.info("tool_call", tool_name="query_rag", latency_ms=latency_ms, result_preview=str(result)[:200])
return result
def list_documents() -> dict:
"""
Return pipeline statistics: accurate totals plus a sample of recent documents.
total_documents and total_chunks_embedded always reflect ALL indexed content.
"""
start = time.monotonic()
SAMPLE = 15 # documents to include in the sample list
try:
with Session(get_engine()) as db:
stmt = select(Job).where(
Job.status == JobStatus.completed,
Job.chunk_count > 0,
)
jobs = db.exec(stmt).all()
total_docs = len(jobs)
total_chunks = sum(j.chunk_count or 0 for j in jobs)
result = {
"total_documents": total_docs,
"total_chunks_embedded": total_chunks,
"sample_documents": [
{
"filename": j.filename,
"file_type": j.file_type,
"chunk_count": j.chunk_count,
}
for j in jobs[:SAMPLE]
],
}
except Exception as exc:
log.error("tool_call_error", tool_name="list_documents", error=str(exc))
result = {"total_documents": 0, "total_chunks_embedded": 0, "sample_documents": []}
latency_ms = int((time.monotonic() - start) * 1000)
log.info("tool_call", tool_name="list_documents", latency_ms=latency_ms,
total_docs=result["total_documents"], total_chunks=result["total_chunks_embedded"])
return result
def summarize_document(job_id: str) -> dict:
"""
Retrieve the structured summary for a processed document.
Args:
job_id: UUID of the completed job.
Returns:
The structured summary dict generated during processing (varies by file type).
"""
start = time.monotonic()
try:
try:
jid = uuid.UUID(job_id)
except ValueError:
return {"error": f"Invalid job_id: {job_id}"}
with Session(get_engine()) as db:
job = db.get(Job, jid)
if not job:
result = {"error": f"Job not found: {job_id}"}
elif job.status != JobStatus.completed:
result = {"error": f"Job {job_id} is not COMPLETED (status: {job.status.value})"}
else:
try:
summary = json.loads(job.result) if job.result else {}
except Exception:
summary = {"raw": job.result}
result = {
"job_id": str(job.id),
"filename": job.filename,
"file_type": job.file_type,
"summary": summary,
}
except Exception as exc:
result = {"error": str(exc)}
latency_ms = int((time.monotonic() - start) * 1000)
log.info("tool_call", tool_name="summarize_document", latency_ms=latency_ms, result_preview=str(result)[:200])
return result