File size: 6,842 Bytes
af8ac78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """CrewAI tools β document ingestion, vector search, graph search, LLM synthesis."""
from __future__ import annotations
import json, uuid, sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from crewai.tools import tool
import config
from pipeline import document_loader, chunker, embedder, vector_store, graph_store
from agents.llm import get_llm
# ββ Ingestion Tools ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool("IngestDocumentTool")
def ingest_document(file_path: str) -> str:
"""Load, chunk, embed, and store a document in the vector database.
Input: absolute path to the document file.
Returns: ingestion summary string.
"""
try:
doc_id = uuid.uuid4().hex[:8]
docs = document_loader.load_document(file_path)
chunks = chunker.chunk_documents(docs)
if not chunks:
return f"No text extracted from {file_path}"
texts = [c["text"] for c in chunks]
embeddings = embedder.embed_texts(texts)
session_token = config.current_session.get()
added = vector_store.add_chunks(chunks, embeddings, doc_id, tier="extended", session_token=session_token)
return (f"Ingested '{os.path.basename(file_path)}': "
f"{len(docs)} pages β {added} chunks stored (id={doc_id})")
except Exception as e:
return f"Ingestion failed: {e}"
@tool("ExtractAndStoreEntitiesTool")
def extract_and_store_entities(file_path: str) -> str:
"""Extract key entities from a document and store in the graph database.
Input: absolute path to the document file.
Returns: entity extraction summary.
"""
if not graph_store.is_available():
return "Graph DB unavailable β skipped entity extraction."
try:
docs = document_loader.load_document(file_path)
source = os.path.basename(file_path)
# Sample first 3 pages for entity extraction (avoid huge prompts)
sample_text = "\n\n".join(d["text"] for d in docs[:3])[:3000]
llm = get_llm()
prompt = (
"Extract key entities from the text below.\n"
"Return a JSON array of objects with keys: name, type, relations.\n"
"type must be a broad category like: Person, Organization, Location, Concept, Event, Document, Object, Rule.\n"
"relations is a list of {target, rel} objects.\n"
"Return ONLY the JSON array, no explanation.\n\n"
f"TEXT:\n{sample_text}\n\nJSON:"
)
raw = llm.call([{"role": "user", "content": prompt}])
# Find JSON array in the response
start = raw.find("[")
end = raw.rfind("]") + 1
if start == -1 or end == 0:
return "No entities extracted (LLM returned no JSON)."
entities = json.loads(raw[start:end])
session_token = config.current_session.get()
graph_store.store_entities(entities, source, tier="extended", session_token=session_token)
return f"Stored {len(entities)} entities from '{source}' in graph DB."
except Exception as e:
return f"Entity extraction failed: {e}"
# ββ Retrieval Tools ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool("VectorSearchTool")
def vector_search(query: str) -> str:
"""Search the vector database for relevant text chunks.
Input: query string.
Returns: formatted context passages with source citations.
"""
try:
q_emb = embedder.embed_query(query)
session_token = config.current_session.get()
results = vector_store.query(q_emb, top_k=config.TOP_K_VECTOR, keyword=query, session_token=session_token)
if not results:
return "No relevant documents found in vector store."
passages = []
for i, r in enumerate(results, 1):
src = r["metadata"].get("source", "unknown")
score = r["score"]
passages.append(f"[{i}] (source: {src}, relevance: {score:.2f})\n{r['text']}")
return "\n\n---\n\n".join(passages)
except Exception as e:
return f"Vector search failed: {e}"
@tool("GraphSearchTool")
def graph_search(entities: str) -> str:
"""Search the graph database for related entities.
Input: comma-separated entity names.
Returns: related entity context or unavailable message.
"""
if not graph_store.is_available():
return "Graph DB unavailable."
try:
names = [e.strip() for e in entities.split(",") if e.strip()]
session_token = config.current_session.get()
related = graph_store.query_related(names, hops=2, session_token=session_token)
if not related:
return "No graph relationships found."
return "Related entities from knowledge graph:\n" + "\n".join(f"- {r}" for r in related)
except Exception as e:
return f"Graph search failed: {e}"
# ββ Synthesis Tool βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@tool("SynthesizeAnswerTool")
def synthesize_answer(context_and_query: str) -> str:
"""Synthesize a final answer from retrieved context.
Input: JSON string with keys 'query' and 'context'.
Returns: Markdown-formatted answer with citations.
"""
try:
data = json.loads(context_and_query)
query = data.get("query", "")
context = data.get("context", "")
except Exception:
query, context = context_and_query, ""
llm = get_llm()
prompt = (
"You are an expert Information Analyst.\n"
"Your task is to answer the question using ONLY the provided context.\n"
"CRITICAL INSTRUCTIONS:\n"
"1. STRICT GROUNDING: You must not use any external knowledge. If the information is not present in the context, do not hallucinate or make assumptions.\n"
"2. ZERO RETRIEVAL GUARDRAIL: If the provided context is empty, irrelevant, or does not contain the answer, you must output EXACTLY and ONLY this sentence:\n"
"'Internal data does not have any information to answer the question.'\n"
"3. FORMAT: If you can answer the question based on the context, format your response in Markdown with a clear structure, bullet points for key facts, source citations like [Source: filename], and a 'Summary' section at the end.\n\n"
f"CONTEXT:\n{context}\n\n"
f"QUESTION: {query}\n\n"
"ANSWER:"
)
return llm.call([{"role": "user", "content": prompt}])
|