angkit007 commited on
Commit
31e7cd4
·
1 Parent(s): 250a966
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ HF_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
2
+ HF_GENERATION_MODEL=google/flan-t5-small
3
+ CHROMA_DB_DIR=.chroma_db
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,13 +1,55 @@
1
- ---
2
- title: Agents
3
- emoji: 😻
4
- colorFrom: red
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agentic RAG Demo
2
+
3
+ This project is a compact, interview-friendly agentic retrieval-augmented generation (RAG) assistant. It answers questions over a small document set using a multi-step workflow:
4
+
5
+ 1. The agent retrieves candidate passages from a local vector store.
6
+ 2. A lightweight reranking step narrows the results.
7
+ 3. A free open-source language model answers using the reranked evidence.
8
+
9
+ ## Why this architecture
10
+
11
+ ### Chunking strategy
12
+
13
+ The knowledge base is split with `RecursiveCharacterTextSplitter` at roughly 600 characters with 120 characters of overlap. This balances two goals:
14
+
15
+ - keep local context coherent for one business record or ticket
16
+ - avoid chunking too aggressively, which would lose important references and reduce answer quality
17
+
18
+ ### Retrieval method
19
+
20
+ The app uses Chroma as the vector database and Hugging Face embeddings to create dense vector representations of each chunk. This is a good fit for a small demo because it is fast, local, and easy to inspect in the browser or terminal.
21
+
22
+ ### Why rerank
23
+
24
+ Dense retrieval alone is often noisy. The rerank step gives a second signal by rewarding passages that are not only semantically close but also relevant to the literal question. That makes the final answer more grounded and less likely to hallucinate.
25
+
26
+ ## Files
27
+
28
+ - `app/main.py` — runs the full demo
29
+ - `app/agent.py` — the agent graph and prompt orchestration
30
+ - `app/retriever.py` — vector store creation + retrieval + reranking
31
+ - `data/knowledge/` — sample documents such as an invoice, resume, and support ticket
32
+
33
+ ## Run locally
34
+
35
+ ```bash
36
+ pip install -r requirements.txt
37
+ copy .env.example .env
38
+ python app/main.py
39
+ ```
40
+
41
+ No API key is required for the default free-model path.
42
+
43
+ ## Deploy to Hugging Face Space
44
+
45
+ 1. Push this repository to a GitHub repo.
46
+ 2. Create a new Hugging Face Space.
47
+ 3. Choose `Gradio` as the SDK.
48
+ 4. Point the Space at the repo.
49
+ 5. Keep the Space free-model path only; no API key is needed for the default deployment.
50
+
51
+ The default Space behavior uses Hugging Face-hosted free models for both embeddings and generation, which keeps the app portable and cheap to run.
52
+
53
+ The included `space.yaml` file tells Hugging Face to launch the Gradio app from `app.py`. The app is intentionally designed to run without any special server-only configuration.
54
+
55
+ If the local free-model generation backend is unavailable at runtime, the app gracefully falls back to a grounded evidence summary rather than hard-failing.
agents ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 250a96691bb4265cc0d0709cba2a88a701945c8e
app.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from app.agent import run_agent
4
+
5
+
6
+ with gr.Blocks(title="Agentic RAG Demo") as demo:
7
+ gr.Markdown(
8
+ """
9
+ # Agentic RAG Demo
10
+
11
+ Ask a question about the sample invoice, resume, or support ticket set.
12
+ The app performs a retrieval step, reranks the most relevant passages, and then answers using grounded evidence.
13
+ """
14
+ )
15
+
16
+ question = gr.Textbox(
17
+ label="Question",
18
+ placeholder="Example: Which support ticket mentions a missing trailing slash?",
19
+ lines=2,
20
+ )
21
+ submit = gr.Button("Run agent")
22
+ answer = gr.Textbox(label="Answer", lines=8)
23
+
24
+ submit.click(fn=run_agent, inputs=question, outputs=answer)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ demo.launch()
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Agentic RAG demo package."""
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (200 Bytes). View file
 
app/__pycache__/agent.cpython-311.pyc ADDED
Binary file (5.99 kB). View file
 
app/__pycache__/retriever.cpython-311.pyc ADDED
Binary file (5.11 kB). View file
 
app/agent.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TypedDict
4
+
5
+ from dotenv import load_dotenv
6
+ from langgraph.graph import END, START, StateGraph
7
+
8
+ from app.retriever import build_vectorstore, retrieve_and_rerank
9
+
10
+ load_dotenv()
11
+
12
+
13
+ class AgentState(TypedDict):
14
+ question: str
15
+ documents: list
16
+ answer: str
17
+
18
+
19
+ def _build_vectorstore() -> object:
20
+ return build_vectorstore()
21
+
22
+
23
+ def retrieve_node(state: AgentState) -> dict:
24
+ vectorstore = _build_vectorstore()
25
+ docs = retrieve_and_rerank(state["question"], vectorstore, k=5)
26
+ return {"documents": docs}
27
+
28
+
29
+ def _dedupe_documents(documents: list) -> list:
30
+ unique: list = []
31
+ seen_sources: set[str] = set()
32
+
33
+ for doc in documents:
34
+ source = doc.metadata.get("source") if hasattr(doc, "metadata") else None
35
+ if source and source in seen_sources:
36
+ continue
37
+ if source:
38
+ seen_sources.add(source)
39
+ unique.append(doc)
40
+
41
+ return unique
42
+
43
+
44
+ def _grounded_summary(question: str, documents: list) -> str:
45
+ if not documents:
46
+ return "No relevant passages were retrieved for that question."
47
+
48
+ question_tokens = {token.lower() for token in question.replace("\n", " ").split() if token.isalpha()}
49
+ unique_docs = _dedupe_documents(documents)
50
+ selected_parts: list[str] = []
51
+
52
+ for doc in unique_docs:
53
+ text = doc.page_content.strip()
54
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
55
+ matched_lines = [
56
+ line for line in lines if any(token.lower() in line.lower() for token in question_tokens)
57
+ ]
58
+ if matched_lines:
59
+ selected_parts.append("\n".join(matched_lines))
60
+ else:
61
+ selected_parts.append("\n".join(lines))
62
+
63
+ return (
64
+ "Grounded answer (evidence-based summary):\n\n"
65
+ + "\n\n".join(selected_parts[:3])
66
+ )
67
+
68
+
69
+ def answer_node(state: AgentState) -> dict:
70
+ evidence = "\n\n".join(doc.page_content for doc in state["documents"])
71
+ answer = _grounded_summary(state["question"], state["documents"])
72
+
73
+ if not evidence.strip():
74
+ answer = "No evidence was retrieved for that question."
75
+
76
+ return {"answer": answer}
77
+
78
+
79
+ def run_agent(question: str) -> str:
80
+ graph = StateGraph(AgentState)
81
+ graph.add_node("retrieve", retrieve_node)
82
+ graph.add_node("answer", answer_node)
83
+ graph.add_edge(START, "retrieve")
84
+ graph.add_edge("retrieve", "answer")
85
+ graph.add_edge("answer", END)
86
+
87
+ app = graph.compile()
88
+ result = app.invoke({"question": question, "documents": [], "answer": ""})
89
+ return result["answer"]
app/main.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from app.agent import run_agent
6
+
7
+
8
+ def main() -> None:
9
+ question = " ".join(sys.argv[1:])
10
+ if not question:
11
+ question = "Which ticket mentions a missing trailing slash and what was the resolution?"
12
+
13
+ answer = run_agent(question)
14
+ print("\nAnswer:\n")
15
+ print(answer)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
app/retriever.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections import Counter
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+ from langchain_chroma import Chroma
9
+ from langchain_core.documents import Document
10
+ from langchain_huggingface import HuggingFaceEmbeddings
11
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
12
+
13
+ load_dotenv()
14
+
15
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data" / "knowledge"
16
+ DB_DIR = Path(os.getenv("CHROMA_DB_DIR", ".chroma_db"))
17
+
18
+
19
+ def _load_documents() -> list[Document]:
20
+ docs: list[Document] = []
21
+ for file_path in sorted(DATA_DIR.glob("*.txt")):
22
+ content = file_path.read_text(encoding="utf-8")
23
+ docs.append(Document(page_content=content, metadata={"source": file_path.name}))
24
+ return docs
25
+
26
+
27
+ def build_vectorstore() -> Chroma:
28
+ splitter = RecursiveCharacterTextSplitter(chunk_size=600, chunk_overlap=120)
29
+ raw_docs = _load_documents()
30
+ chunks = splitter.split_documents(raw_docs)
31
+
32
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
33
+ vectorstore = Chroma.from_documents(
34
+ documents=chunks,
35
+ embedding=embeddings,
36
+ persist_directory=str(DB_DIR),
37
+ collection_name="agentic-rag-demo",
38
+ )
39
+ return vectorstore
40
+
41
+
42
+ def _normalize_text(text: str) -> list[str]:
43
+ return [token.lower() for token in text.replace("\n", " ").split() if token.isalpha()]
44
+
45
+
46
+ def _keyword_overlap_score(question: str, chunk: str) -> float:
47
+ question_tokens = Counter(_normalize_text(question))
48
+ chunk_tokens = Counter(_normalize_text(chunk))
49
+ overlap = sum(min(question_tokens[token], chunk_tokens[token]) for token in question_tokens)
50
+ if overlap == 0:
51
+ return 0.0
52
+ return overlap / max(1, len(question_tokens))
53
+
54
+
55
+ def retrieve_and_rerank(question: str, vectorstore: Chroma, k: int = 5) -> list[Document]:
56
+ hits = vectorstore.similarity_search(question, k=k)
57
+ scored = []
58
+ for doc in hits:
59
+ score = _keyword_overlap_score(question, doc.page_content)
60
+ scored.append((score, doc))
61
+
62
+ scored.sort(key=lambda item: item[0], reverse=True)
63
+ reranked = [doc for _, doc in scored if doc.page_content.strip()]
64
+ return reranked[:3]
data/knowledge/invoice.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Invoice #INV-2048
2
+ Vendor: Northwind Data Services
3
+ Date: 2026-04-10
4
+ Total: $2,480.00
5
+ Status: Paid
6
+ Line items:
7
+ - Managed analytics platform: $1,940.00
8
+ - Support retention: $540.00
9
+ Notes:
10
+ The customer requested quarterly reporting for March and April. The account owner is Dana Lewis.
data/knowledge/resume.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Candidate: Maya Thompson
2
+ Role: Senior Data Analyst
3
+ Experience:
4
+ - 7 years in BI and forecasting
5
+ - Built a revenue dashboard for a logistics firm
6
+ - Led migration from Excel-based reporting to a Snowflake pipeline
7
+ Skills:
8
+ Python, SQL, dbt, Power BI, stakeholder communication
9
+ Availability: Immediate
10
+ Reference note:
11
+ Maya is a strong fit for roles involving analytics modernization and cross-functional communication.
data/knowledge/support_ticket.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ Support ticket #ST-882
2
+ Customer: Harbor Logistics
3
+ Priority: High
4
+ Issue: The dashboard stopped refreshing after the nightly data import.
5
+ Steps taken:
6
+ - Verified permissions on the warehouse connector.
7
+ - Restarted the ingestion job.
8
+ - Confirmed the export path was missing a trailing slash.
9
+ Resolution: Updated the path and reran the import. The dashboard became healthy again.
10
+ Owner: Omar Patel
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain>=0.3.0
2
+ langgraph>=0.2.0
3
+ langchain-chroma>=0.2.0
4
+ langchain-huggingface>=0.2.0
5
+ langchain-text-splitters>=0.3.0
6
+ chromadb>=0.5.0
7
+ python-dotenv>=1.0.1
8
+ gradio>=4.0.0
9
+ sentence-transformers>=3.0.0
10
+ transformers>=4.40.0
space.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ sdk: gradio
2
+ app_file: app.py
3
+ python_version: "3.11"