Devisri515 commited on
Commit
41e79c8
·
1 Parent(s): 38b974d

user uploads supported

Browse files
Files changed (5) hide show
  1. app.py +104 -109
  2. requirements.txt +1 -0
  3. src/agent.py +23 -24
  4. src/file_processor.py +72 -97
  5. src/main.py +54 -29
app.py CHANGED
@@ -3,146 +3,141 @@ import logging
3
  import gradio as gr
4
  import requests
5
 
6
- # Suppress HuggingFace tokenizer parallelism warning
7
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
8
 
9
- # Setup logging
10
  logging.basicConfig(level=logging.INFO)
11
  logger = logging.getLogger(__name__)
12
 
13
- # FastAPI endpoint configuration
14
  FASTAPI_URL = os.getenv("FASTAPI_URL", "http://127.0.0.1:8000")
15
  CHAT_ENDPOINT = f"{FASTAPI_URL}/chat"
 
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- def determine_source(response_text) -> str:
19
- """
20
- Attempt to determine if the response came from RAG or Web Search.
21
- Looks for keywords or source indicators in the response.
22
- """
23
- # Convert list to string if needed
24
- if isinstance(response_text, list):
25
- response_text = " ".join(str(item) for item in response_text)
26
-
27
- response_lower = str(response_text).lower()
28
-
29
- # Check for web search indicators
30
- if any(keyword in response_lower for keyword in ["search", "web", "duckduckgo", "internet", "online"]):
31
- return "Source: Web Search"
32
-
33
- # Check for RAG/document indicators
34
- if "source:" in response_lower and "page" in response_lower:
35
- return "Source: Internal Documents (RAG)"
36
-
37
- if any(keyword in response_lower for keyword in ["document", "policy", "pdf", "internal", "knowledge base"]):
38
- return "Source: Internal Documents (RAG)"
39
-
40
- # Default fallback - cannot determine
41
- return "Source: Mixed (RAG + Web Search)"
42
 
43
  def process_query(message: str, chat_history: list) -> tuple[list, str]:
44
- """
45
- Process user query by calling the FastAPI /chat endpoint.
46
-
47
- Args:
48
- message: User query
49
- chat_history: Current chat history in Gradio format
50
-
51
- Returns:
52
- Tuple of (updated_chat_history, status_message)
53
- """
54
  try:
55
- logger.info(f"Sending query to FastAPI: {message}")
56
- response = requests.post(CHAT_ENDPOINT, json={"query": message}, timeout=60)
57
- response.raise_for_status()
58
- payload = response.json()
59
- response_text = payload.get("response", "")
60
-
61
- if isinstance(response_text, list):
62
- response_text = "\n".join(str(item) for item in response_text)
63
- else:
64
- response_text = str(response_text)
65
-
66
  source = determine_source(response_text)
67
- full_response = f"{response_text}\n\n--- {source} ---"
68
-
69
  chat_history.append({"role": "user", "content": message})
70
  chat_history.append({"role": "assistant", "content": full_response})
71
-
72
- logger.info("Response received from FastAPI")
73
- return chat_history, f"Query processed. {source}"
74
  except requests.exceptions.RequestException as e:
75
- error_msg = f"FastAPI request failed: {str(e)}"
76
- logger.error(error_msg)
77
- chat_history.append({"role": "user", "content": message})
78
- chat_history.append({"role": "assistant", "content": f"ERROR: {error_msg}"})
79
- return chat_history, error_msg
80
- except Exception as e:
81
- error_msg = f"Error processing query: {str(e)}"
82
- logger.error(error_msg)
83
  chat_history.append({"role": "user", "content": message})
84
- chat_history.append({"role": "assistant", "content": f"ERROR: {error_msg}"})
85
- return chat_history, error_msg
 
86
 
87
  def clear_chat() -> tuple[list, str]:
88
- """Clear chat history."""
89
  return [], ""
90
 
91
- # Build Gradio Interface
 
92
  with gr.Blocks(title="Agentic RAG Knowledge Search") as demo:
93
- gr.Markdown("# 🔍 Agentic RAG Knowledge Search")
94
- gr.Markdown("Ask questions answered from internal documents (RAG) or the live web — the agent decides.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  with gr.Group():
 
96
  chatbot = gr.Chatbot(
97
- label="Conversation History",
98
- show_label=True,
99
- height=400
100
  )
101
-
102
- with gr.Row():
103
- user_input = gr.Textbox(
104
- placeholder="Ask a question about policy documents or anything on the web...",
105
- label="Your Question",
106
- lines=2
107
- )
108
-
109
- with gr.Row():
110
- submit_btn = gr.Button("Submit", variant="primary")
111
- clear_btn = gr.Button("Clear", variant="secondary")
112
-
113
- status_output = gr.Textbox(
114
- label="Status",
115
- interactive=False,
116
- lines=1
117
- )
118
-
119
- # Wire up chat interactions
120
  submit_btn.click(
121
  fn=process_query,
122
  inputs=[user_input, chatbot],
123
- outputs=[chatbot, status_output]
124
- ).then(
125
- fn=lambda: "",
126
- inputs=[],
127
- outputs=[user_input]
128
- )
129
-
130
- clear_btn.click(
131
- fn=clear_chat,
132
- inputs=[],
133
- outputs=[chatbot, status_output]
134
- )
135
-
136
- # Allow Enter key to submit
137
  user_input.submit(
138
  fn=process_query,
139
  inputs=[user_input, chatbot],
140
- outputs=[chatbot, status_output]
141
- ).then(
142
- fn=lambda: "",
143
- inputs=[],
144
- outputs=[user_input]
145
- )
146
 
147
  if __name__ == "__main__":
148
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False, theme=gr.themes.Soft())
 
3
  import gradio as gr
4
  import requests
5
 
 
6
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
7
 
 
8
  logging.basicConfig(level=logging.INFO)
9
  logger = logging.getLogger(__name__)
10
 
 
11
  FASTAPI_URL = os.getenv("FASTAPI_URL", "http://127.0.0.1:8000")
12
  CHAT_ENDPOINT = f"{FASTAPI_URL}/chat"
13
+ UPLOAD_ENDPOINT = f"{FASTAPI_URL}/upload"
14
+ RESET_ENDPOINT = f"{FASTAPI_URL}/reset"
15
 
16
+ SUPPORTED_TYPES = [".pdf", ".docx", ".txt", ".md", ".csv"]
17
+
18
+
19
+ def upload_files(files) -> str:
20
+ if not files:
21
+ return "No files selected."
22
+ try:
23
+ multipart = []
24
+ for f in files:
25
+ path = f if isinstance(f, str) else f.name
26
+ filename = os.path.basename(path)
27
+ with open(path, "rb") as fp:
28
+ multipart.append(("files", (filename, fp.read(), "application/octet-stream")))
29
+
30
+ resp = requests.post(UPLOAD_ENDPOINT, files=multipart, timeout=120)
31
+ resp.raise_for_status()
32
+ return resp.json().get("status", "Files processed.")
33
+ except requests.exceptions.RequestException as e:
34
+ return f"Upload failed: {e}"
35
+
36
+
37
+ def reset_documents() -> str:
38
+ try:
39
+ resp = requests.post(RESET_ENDPOINT, timeout=10)
40
+ resp.raise_for_status()
41
+ return resp.json().get("status", "Documents cleared.")
42
+ except requests.exceptions.RequestException as e:
43
+ return f"Reset failed: {e}"
44
+
45
+
46
+ def determine_source(text: str) -> str:
47
+ lower = text.lower()
48
+ if any(k in lower for k in ["search", "web", "duckduckgo", "internet", "online"]):
49
+ return "Web Search"
50
+ if any(k in lower for k in ["uploaded", "file:", "page", "document", "policy", "pdf"]):
51
+ return "Uploaded Documents (RAG)"
52
+ return "RAG + Web Search"
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  def process_query(message: str, chat_history: list) -> tuple[list, str]:
56
+ if not message.strip():
57
+ return chat_history, "Please enter a question."
 
 
 
 
 
 
 
 
58
  try:
59
+ resp = requests.post(CHAT_ENDPOINT, json={"query": message}, timeout=120)
60
+ resp.raise_for_status()
61
+ response_text = resp.json().get("response", "")
62
+
 
 
 
 
 
 
 
63
  source = determine_source(response_text)
64
+ full_response = f"{response_text}\n\n--- Source: {source} ---"
65
+
66
  chat_history.append({"role": "user", "content": message})
67
  chat_history.append({"role": "assistant", "content": full_response})
68
+ return chat_history, f"Done — answered via {source}"
 
 
69
  except requests.exceptions.RequestException as e:
70
+ error = f"Request failed: {e}"
 
 
 
 
 
 
 
71
  chat_history.append({"role": "user", "content": message})
72
+ chat_history.append({"role": "assistant", "content": f"ERROR: {error}"})
73
+ return chat_history, error
74
+
75
 
76
  def clear_chat() -> tuple[list, str]:
 
77
  return [], ""
78
 
79
+
80
+ # --- UI ---
81
  with gr.Blocks(title="Agentic RAG Knowledge Search") as demo:
82
+ gr.Markdown("# Agentic RAG Knowledge Search")
83
+ gr.Markdown(
84
+ "Upload your documents, then ask questions. "
85
+ "The agent searches your files first, then the web if needed."
86
+ )
87
+
88
+ # File upload panel
89
+ with gr.Group():
90
+ gr.Markdown("### Upload Documents")
91
+ gr.Markdown(f"Supported: {', '.join(SUPPORTED_TYPES)}")
92
+ with gr.Row():
93
+ file_input = gr.File(
94
+ label="Select Files",
95
+ file_count="multiple",
96
+ file_types=SUPPORTED_TYPES,
97
+ )
98
+ with gr.Row():
99
+ upload_btn = gr.Button("Process Files", variant="primary")
100
+ reset_btn = gr.Button("Clear Uploaded Documents", variant="secondary")
101
+ upload_status = gr.Textbox(label="Upload Status", interactive=False, lines=2)
102
+
103
+ gr.Markdown("---")
104
+
105
+ # Chat panel
106
  with gr.Group():
107
+ gr.Markdown("### Ask a Question")
108
  chatbot = gr.Chatbot(
109
+ label="Conversation",
110
+ height=420,
 
111
  )
112
+ with gr.Row():
113
+ user_input = gr.Textbox(
114
+ placeholder="Ask anything about your documents or the web...",
115
+ label="Your Question",
116
+ lines=2,
117
+ scale=4,
118
+ )
119
+ submit_btn = gr.Button("Submit", variant="primary", scale=1)
120
+ with gr.Row():
121
+ clear_btn = gr.Button("Clear Chat", variant="secondary")
122
+ status_output = gr.Textbox(label="Status", interactive=False, lines=1)
123
+
124
+ # Wiring
125
+ upload_btn.click(fn=upload_files, inputs=[file_input], outputs=[upload_status])
126
+ reset_btn.click(fn=reset_documents, inputs=[], outputs=[upload_status])
127
+
 
 
 
128
  submit_btn.click(
129
  fn=process_query,
130
  inputs=[user_input, chatbot],
131
+ outputs=[chatbot, status_output],
132
+ ).then(fn=lambda: "", inputs=[], outputs=[user_input])
133
+
 
 
 
 
 
 
 
 
 
 
 
134
  user_input.submit(
135
  fn=process_query,
136
  inputs=[user_input, chatbot],
137
+ outputs=[chatbot, status_output],
138
+ ).then(fn=lambda: "", inputs=[], outputs=[user_input])
139
+
140
+ clear_btn.click(fn=clear_chat, inputs=[], outputs=[chatbot, status_output])
 
 
141
 
142
  if __name__ == "__main__":
143
  demo.launch(server_name="0.0.0.0", server_port=7860, share=False, theme=gr.themes.Soft())
requirements.txt CHANGED
@@ -15,6 +15,7 @@ langchain-huggingface>=0.1.2
15
  sentence-transformers>=3.2.0
16
  faiss-cpu>=1.7.4
17
  pypdf>=5.1.0
 
18
  duckduckgo-search==5.3.1
19
  pydantic>=2.9.0
20
  requests>=2.32.0
 
15
  sentence-transformers>=3.2.0
16
  faiss-cpu>=1.7.4
17
  pypdf>=5.1.0
18
+ docx2txt>=0.8
19
  duckduckgo-search==5.3.1
20
  pydantic>=2.9.0
21
  requests>=2.32.0
src/agent.py CHANGED
@@ -5,52 +5,51 @@ from langchain_community.tools import DuckDuckGoSearchRun
5
  from langgraph.prebuilt import create_react_agent
6
  from dotenv import load_dotenv
7
  from src.rag_engine import KnowledgeBase
 
8
 
9
  load_dotenv()
10
 
11
- # --- Configuration ---
12
- PDF_PATH = os.path.join("data", "policy.pdf")
 
13
 
14
- kb = KnowledgeBase(pdf_path=PDF_PATH)
 
 
15
  try:
16
- kb.load_and_index()
17
  except Exception as e:
18
- print(f"PDF Load skipped: {e}")
19
 
20
- # --- Define Tools ---
21
 
22
  @tool
23
- def lookup_internal_policy(query: str) -> str:
24
- """Useful for answering questions about specific internal policies, documents, laws, or the PDF file."""
25
- return kb.retrieve(query)
 
 
 
 
 
 
26
 
27
- # Initialize the tool ONCE (Global scope)
28
  search_tool = DuckDuckGoSearchRun()
29
 
30
  @tool
31
  def search_web(query: str) -> str:
32
- """Useful for finding current events, news, or general knowledge not in the internal docs."""
33
- # Use the pre-initialized tool
34
  try:
35
  return search_tool.run(query)
36
  except Exception as e:
37
  return f"Search failed: {e}"
38
 
39
- # --- Initialize Agent ---
40
 
41
  def get_agent_executor():
42
  if not os.getenv("GOOGLE_API_KEY"):
43
  raise ValueError("GOOGLE_API_KEY not found in .env file")
44
 
45
  print("Initializing Gemini Agent (Model: gemini-2.5-flash-lite)...")
46
- llm = ChatGoogleGenerativeAI(
47
- model="gemini-2.5-flash-lite",
48
- temperature=0
49
- )
50
-
51
- tools = [lookup_internal_policy, search_web]
52
-
53
- # Create the Agent (LangGraph)
54
- agent = create_react_agent(llm, tools)
55
-
56
- return agent
 
5
  from langgraph.prebuilt import create_react_agent
6
  from dotenv import load_dotenv
7
  from src.rag_engine import KnowledgeBase
8
+ from src.file_processor import FileProcessor
9
 
10
  load_dotenv()
11
 
12
+ # --- Shared state ---
13
+ # file_processor is imported and mutated by main.py's /upload endpoint
14
+ file_processor = FileProcessor()
15
 
16
+ # Optional fallback KB (original policy.pdf); silently skipped if missing
17
+ _PDF_PATH = os.path.join("data", "policy.pdf")
18
+ _fallback_kb = KnowledgeBase(pdf_path=_PDF_PATH)
19
  try:
20
+ _fallback_kb.load_and_index()
21
  except Exception as e:
22
+ print(f"Fallback KB skipped: {e}")
23
 
24
+ # --- Tools ---
25
 
26
  @tool
27
+ def lookup_documents(query: str) -> str:
28
+ """Search the user-uploaded documents for relevant information.
29
+ Use this for questions about content in any uploaded files."""
30
+ if file_processor.has_documents():
31
+ result = file_processor.retrieve(query)
32
+ if result:
33
+ return result
34
+ # Fall back to original KB when no uploads exist
35
+ return _fallback_kb.retrieve(query)
36
 
 
37
  search_tool = DuckDuckGoSearchRun()
38
 
39
  @tool
40
  def search_web(query: str) -> str:
41
+ """Search the web for current events, news, or general knowledge not in uploaded documents."""
 
42
  try:
43
  return search_tool.run(query)
44
  except Exception as e:
45
  return f"Search failed: {e}"
46
 
47
+ # --- Agent factory ---
48
 
49
  def get_agent_executor():
50
  if not os.getenv("GOOGLE_API_KEY"):
51
  raise ValueError("GOOGLE_API_KEY not found in .env file")
52
 
53
  print("Initializing Gemini Agent (Model: gemini-2.5-flash-lite)...")
54
+ llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash-lite", temperature=0)
55
+ return create_react_agent(llm, [lookup_documents, search_web])
 
 
 
 
 
 
 
 
 
src/file_processor.py CHANGED
@@ -1,136 +1,111 @@
1
- """
2
- File Processor for handling user-uploaded documents.
3
- Processes PDFs, creates FAISS indices for semantic search.
4
- """
5
-
6
  import logging
7
- from langchain_community.document_loaders import PyPDFLoader
 
 
 
 
 
 
8
  from langchain_text_splitters import RecursiveCharacterTextSplitter
9
  from langchain_community.vectorstores import FAISS
10
  from langchain_huggingface import HuggingFaceEmbeddings
11
 
12
  logger = logging.getLogger(__name__)
13
 
 
14
 
15
- class FileProcessor:
16
- """
17
- Handles processing of user-uploaded PDF files and creates searchable indices.
18
- """
19
 
20
- def __init__(self, embedding_model: str = "all-MiniLM-L6-v2"):
21
- """
22
- Initialize FileProcessor with embeddings.
 
 
 
 
 
 
 
 
23
 
24
- Args:
25
- embedding_model: HuggingFace embedding model name
26
- """
27
- logger.info(f"Initializing FileProcessor with model: {embedding_model}")
28
  self.embeddings = HuggingFaceEmbeddings(model_name=embedding_model)
29
  self.vector_store = None
30
  self.status = "No files processed"
31
-
32
- def process_files(self, files: list) -> str:
33
- """
34
- Process uploaded PDF files and create a FAISS index.
35
-
36
- Args:
37
- files: List of file objects from Gradio
38
-
39
- Returns:
40
- Status message
41
- """
42
- if not files:
43
- self.status = "No files uploaded"
44
- return "No files selected. Using internal docs + web search."
45
-
46
- try:
47
- logger.info(f"Processing {len(files)} uploaded file(s)...")
48
- all_docs = []
49
-
50
- # Load all PDF files
51
- for file_obj in files:
52
- file_path = file_obj if isinstance(file_obj, str) else file_obj.name
53
-
54
- if file_path.lower().endswith(".pdf"):
55
- logger.info(f"Loading PDF: {file_path}")
56
- loader = PyPDFLoader(file_path)
57
- docs = loader.load()
58
- all_docs.extend(docs)
59
- else:
60
- logger.warning(f"Skipping non-PDF file: {file_path}")
61
-
62
- if not all_docs:
63
- self.status = "No PDFs found in uploads"
64
- return "No valid PDF files uploaded. Using internal docs + web search."
65
-
66
- # Split documents into chunks
67
- logger.info(f"Splitting {len(all_docs)} documents into chunks...")
68
- text_splitter = RecursiveCharacterTextSplitter(
69
- chunk_size=1000, chunk_overlap=200
70
  )
71
- chunks = text_splitter.split_documents(all_docs)
72
 
73
- # Create FAISS index
74
- logger.info(f"Creating FAISS index from {len(chunks)} chunks...")
75
- self.vector_store = FAISS.from_documents(chunks, self.embeddings)
76
 
77
- self.status = (
78
- f"Indexed {len(chunks)} chunks from {len(all_docs)} pages"
79
- )
80
- logger.info(self.status)
81
- return f"✓ SUCCESS: {self.status} - Your uploaded documents will be searched first!"
 
82
 
83
- except Exception as e:
84
- error_msg = f"Error processing files: {str(e)}"
85
- logger.error(error_msg)
86
- self.status = "Error loading files"
87
- return f"ERROR: {error_msg} - Falling back to internal docs + web search."
88
 
89
  def retrieve(self, query: str, k: int = 4) -> str:
90
- """
91
- Retrieve relevant chunks from uploaded files.
92
-
93
- Args:
94
- query: Search query
95
- k: Number of results to return
96
-
97
- Returns:
98
- Retrieved content or empty string if no index
99
- """
100
  if not self.vector_store:
101
  return ""
102
-
103
  try:
104
  docs = self.vector_store.similarity_search(query, k=k)
105
  if not docs:
106
  return ""
107
  return "\n\n".join(
108
- [f"[Uploaded Document] {d.page_content}" for d in docs]
 
109
  )
110
  except Exception as e:
111
- logger.error(f"Error retrieving from uploaded files: {e}")
112
  return ""
113
 
114
  def has_documents(self) -> bool:
115
- """
116
- Check if vector store has documents.
117
-
118
- Returns:
119
- True if documents are loaded, False otherwise
120
- """
121
  return self.vector_store is not None
122
 
123
  def get_status(self) -> str:
124
- """
125
- Get current processing status.
126
-
127
- Returns:
128
- Status message
129
- """
130
  return self.status
131
 
132
  def reset(self) -> None:
133
- """Reset the file processor and clear loaded documents."""
134
  self.vector_store = None
135
  self.status = "No files processed"
136
- logger.info("File processor reset")
 
 
 
 
 
 
1
  import logging
2
+ import os
3
+ from langchain_community.document_loaders import (
4
+ PyPDFLoader,
5
+ TextLoader,
6
+ CSVLoader,
7
+ Docx2txtLoader,
8
+ )
9
  from langchain_text_splitters import RecursiveCharacterTextSplitter
10
  from langchain_community.vectorstores import FAISS
11
  from langchain_huggingface import HuggingFaceEmbeddings
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ SUPPORTED_EXTENSIONS = {".pdf", ".txt", ".md", ".csv", ".docx"}
16
 
 
 
 
 
17
 
18
+ def _loader_for(file_path: str):
19
+ ext = os.path.splitext(file_path)[1].lower()
20
+ if ext == ".pdf":
21
+ return PyPDFLoader(file_path)
22
+ if ext in (".txt", ".md"):
23
+ return TextLoader(file_path, encoding="utf-8")
24
+ if ext == ".csv":
25
+ return CSVLoader(file_path)
26
+ if ext == ".docx":
27
+ return Docx2txtLoader(file_path)
28
+ return None
29
 
30
+
31
+ class FileProcessor:
32
+ def __init__(self, embedding_model: str = "all-MiniLM-L6-v2"):
 
33
  self.embeddings = HuggingFaceEmbeddings(model_name=embedding_model)
34
  self.vector_store = None
35
  self.status = "No files processed"
36
+ self._splitter = RecursiveCharacterTextSplitter(
37
+ chunk_size=1000, chunk_overlap=200
38
+ )
39
+
40
+ def process_files(self, file_paths: list[str]) -> str:
41
+ """Index a list of file paths into the FAISS vector store."""
42
+ if not file_paths:
43
+ return "No files provided."
44
+
45
+ all_docs = []
46
+ skipped = []
47
+
48
+ for path in file_paths:
49
+ ext = os.path.splitext(path)[1].lower()
50
+ if ext not in SUPPORTED_EXTENSIONS:
51
+ skipped.append(os.path.basename(path))
52
+ continue
53
+ try:
54
+ loader = _loader_for(path)
55
+ docs = loader.load()
56
+ for d in docs:
57
+ d.metadata["source_file"] = os.path.basename(path)
58
+ all_docs.extend(docs)
59
+ logger.info(f"Loaded {len(docs)} page(s) from {os.path.basename(path)}")
60
+ except Exception as e:
61
+ logger.error(f"Failed to load {path}: {e}")
62
+ skipped.append(os.path.basename(path))
63
+
64
+ if not all_docs:
65
+ self.status = "No supported files could be loaded"
66
+ return (
67
+ f"Could not load any files. "
68
+ f"Supported types: {', '.join(sorted(SUPPORTED_EXTENSIONS))}. "
69
+ + (f"Skipped: {', '.join(skipped)}" if skipped else "")
 
 
 
 
 
70
  )
 
71
 
72
+ chunks = self._splitter.split_documents(all_docs)
 
 
73
 
74
+ if self.vector_store is None:
75
+ self.vector_store = FAISS.from_documents(chunks, self.embeddings)
76
+ else:
77
+ # Merge into existing index so previous uploads are retained
78
+ new_store = FAISS.from_documents(chunks, self.embeddings)
79
+ self.vector_store.merge_from(new_store)
80
 
81
+ file_count = len(file_paths) - len(skipped)
82
+ self.status = f"Indexed {len(chunks)} chunks from {file_count} file(s)"
83
+ note = f" (skipped: {', '.join(skipped)})" if skipped else ""
84
+ logger.info(self.status)
85
+ return f"Done: {self.status}{note}"
86
 
87
  def retrieve(self, query: str, k: int = 4) -> str:
 
 
 
 
 
 
 
 
 
 
88
  if not self.vector_store:
89
  return ""
 
90
  try:
91
  docs = self.vector_store.similarity_search(query, k=k)
92
  if not docs:
93
  return ""
94
  return "\n\n".join(
95
+ f"[File: {d.metadata.get('source_file', '?')}] {d.page_content}"
96
+ for d in docs
97
  )
98
  except Exception as e:
99
+ logger.error(f"Retrieval error: {e}")
100
  return ""
101
 
102
  def has_documents(self) -> bool:
 
 
 
 
 
 
103
  return self.vector_store is not None
104
 
105
  def get_status(self) -> str:
 
 
 
 
 
 
106
  return self.status
107
 
108
  def reset(self) -> None:
 
109
  self.vector_store = None
110
  self.status = "No files processed"
111
+ logger.info("FileProcessor reset")
src/main.py CHANGED
@@ -1,62 +1,87 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel
3
- from src.agent import get_agent_executor
4
  import logging
 
 
 
 
 
5
 
6
- # Setup logging
7
  logging.basicConfig(level=logging.INFO)
8
  logger = logging.getLogger(__name__)
9
 
10
  app = FastAPI(
11
  title="Agentic RAG Service",
12
- description="An AI Microservice that routes between Internal Docs and Web Search using LangGraph.",
13
- version="2.0"
14
  )
15
 
16
- # Initialize Agent
17
  try:
18
  agent_executor = get_agent_executor()
19
  except Exception as e:
20
  logger.error(f"Failed to initialize agent: {e}")
21
  agent_executor = None
22
 
23
- # --- API Models ---
24
  class QueryRequest(BaseModel):
25
  query: str
26
 
 
27
  class QueryResponse(BaseModel):
28
  response: str
29
 
30
- # --- Routes ---
31
 
32
  @app.get("/")
33
  async def root():
34
  return {
35
- "status": "active",
36
- "service": "Agentic Knowledge Search (LangGraph)",
37
- "docs_url": "/docs"
 
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  @app.post("/chat", response_model=QueryResponse)
41
  async def chat(request: QueryRequest):
42
  if not agent_executor:
43
- raise HTTPException(status_code=500, detail="Agent not initialized (Check API Keys)")
44
-
45
  try:
46
- logger.info(f"Received query: {request.query}")
47
-
48
- # LangGraph Input Format
49
- # pass a dictionary with "messages"
50
- inputs = {"messages": [("user", request.query)]}
51
-
52
- # Invoke the agent
53
- result = agent_executor.invoke(inputs)
54
-
55
- # The result contains the entire conversation state.
56
- # The last message is the AI's answer.
57
  last_message = result["messages"][-1]
58
 
59
- # Gemini may return content as a list of typed blocks; extract plain text
60
  content = last_message.content
61
  if isinstance(content, list):
62
  content = " ".join(
@@ -66,11 +91,11 @@ async def chat(request: QueryRequest):
66
  )
67
 
68
  return QueryResponse(response=str(content))
69
-
70
  except Exception as e:
71
- logger.error(f"Error processing query: {e}")
72
  raise HTTPException(status_code=500, detail=str(e))
73
 
 
74
  if __name__ == "__main__":
75
  import uvicorn
76
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ import os
2
+ import shutil
3
+ import tempfile
4
  import logging
5
+ from fastapi import FastAPI, HTTPException, UploadFile, File
6
+ from fastapi.responses import JSONResponse
7
+ from pydantic import BaseModel
8
+ from typing import List
9
+ from src.agent import get_agent_executor, file_processor
10
 
 
11
  logging.basicConfig(level=logging.INFO)
12
  logger = logging.getLogger(__name__)
13
 
14
  app = FastAPI(
15
  title="Agentic RAG Service",
16
+ description="AI microservice that routes between uploaded documents and web search.",
17
+ version="3.0",
18
  )
19
 
 
20
  try:
21
  agent_executor = get_agent_executor()
22
  except Exception as e:
23
  logger.error(f"Failed to initialize agent: {e}")
24
  agent_executor = None
25
 
26
+
27
  class QueryRequest(BaseModel):
28
  query: str
29
 
30
+
31
  class QueryResponse(BaseModel):
32
  response: str
33
 
 
34
 
35
  @app.get("/")
36
  async def root():
37
  return {
38
+ "status": "active",
39
+ "service": "Agentic Knowledge Search",
40
+ "docs_url": "/docs",
41
+ "uploaded_docs": file_processor.get_status(),
42
  }
43
 
44
+
45
+ @app.post("/upload")
46
+ async def upload_files(files: List[UploadFile] = File(...)):
47
+ """Accept uploaded files, index them for RAG, return a status message."""
48
+ tmp_dir = tempfile.mkdtemp()
49
+ try:
50
+ saved_paths = []
51
+ for upload in files:
52
+ dest = os.path.join(tmp_dir, upload.filename)
53
+ with open(dest, "wb") as f:
54
+ shutil.copyfileobj(upload.file, f)
55
+ saved_paths.append(dest)
56
+ logger.info(f"Saved upload: {upload.filename}")
57
+
58
+ status = file_processor.process_files(saved_paths)
59
+ logger.info(f"Processing result: {status}")
60
+ return JSONResponse({"status": status})
61
+ except Exception as e:
62
+ logger.error(f"Upload error: {e}")
63
+ raise HTTPException(status_code=500, detail=str(e))
64
+ finally:
65
+ shutil.rmtree(tmp_dir, ignore_errors=True)
66
+
67
+
68
+ @app.post("/reset")
69
+ async def reset_documents():
70
+ """Clear all uploaded documents from the index."""
71
+ file_processor.reset()
72
+ return {"status": "Uploaded documents cleared."}
73
+
74
+
75
  @app.post("/chat", response_model=QueryResponse)
76
  async def chat(request: QueryRequest):
77
  if not agent_executor:
78
+ raise HTTPException(status_code=500, detail="Agent not initialized (check API key)")
79
+
80
  try:
81
+ logger.info(f"Query: {request.query}")
82
+ result = agent_executor.invoke({"messages": [("user", request.query)]})
 
 
 
 
 
 
 
 
 
83
  last_message = result["messages"][-1]
84
 
 
85
  content = last_message.content
86
  if isinstance(content, list):
87
  content = " ".join(
 
91
  )
92
 
93
  return QueryResponse(response=str(content))
 
94
  except Exception as e:
95
+ logger.error(f"Chat error: {e}")
96
  raise HTTPException(status_code=500, detail=str(e))
97
 
98
+
99
  if __name__ == "__main__":
100
  import uvicorn
101
+ uvicorn.run(app, host="0.0.0.0", port=8000)