teohteiktoe commited on
Commit
a3e86ec
·
verified ·
1 Parent(s): 190798f

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +16 -22
  2. app.py +132 -264
  3. requirements.txt +6 -4
README.md CHANGED
@@ -1,26 +1,20 @@
1
- # LangGraph Multi-Agent: Gemini + Salary Prediction
 
 
 
 
 
 
 
 
 
 
2
 
3
- This Hugging Face Space replaces DuckDuckGo with Gemini.
4
 
5
- ## Files
6
- - `app.py` - Gradio + LangGraph application
7
- - `salary_model.joblib` - uploaded trained salary model
8
- - `requirements.txt` - dependencies
9
 
10
- ## Hugging Face Setup
11
- 1. Create a new Hugging Face Space.
12
- 2. Select **Gradio** as the SDK.
13
- 3. Upload all files in this folder.
14
- 4. Go to **Settings > Repository secrets**.
15
- 5. Add:
16
 
17
- ```text
18
- GOOGLE_API_KEY = your_gemini_api_key
19
- ```
20
-
21
- ## Run locally
22
-
23
- ```bash
24
- pip install -r requirements.txt
25
- python app.py
26
- ```
 
1
+ ---
2
+ title: Gemini LangGraph RAG
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "5.0.0"
8
+ python_version: "3.10"
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
 
13
+ # Gemini LangGraph RAG
14
 
15
+ Upload PDF, DOCX, TXT, MD, or CSV files, build a temporary Chroma knowledge base, and ask RAG questions using Gemini and LangGraph.
 
 
 
16
 
17
+ ## Secret Required
18
+ Add this Hugging Face Space secret:
 
 
 
 
19
 
20
+ `GOOGLE_API_KEY = your Gemini API key`
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,291 +1,159 @@
1
  import os
2
- import re
3
- import datetime
4
- from typing import TypedDict
5
 
6
  import gradio as gr
7
- import joblib
8
- import pandas as pd
9
-
10
- from langgraph.graph import StateGraph, END
11
  from langchain_google_genai import ChatGoogleGenerativeAI
 
 
 
 
 
12
 
 
 
 
13
 
14
- # =========================
15
- # 1. Load Gemini
16
- # =========================
17
- GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
18
-
19
- if GOOGLE_API_KEY:
20
- llm = ChatGoogleGenerativeAI(
21
- model="gemini-2.5-flash",
22
- google_api_key=GOOGLE_API_KEY,
23
- temperature=0.2
24
- )
25
- else:
26
- llm = None
27
-
28
-
29
- # =========================
30
- # 2. Load salary model
31
- # =========================
32
- MODEL_PATH = "salary_model.joblib"
33
-
34
- salary_model_artifact = joblib.load(MODEL_PATH)
35
-
36
- if isinstance(salary_model_artifact, dict):
37
- salary_model = salary_model_artifact.get("model")
38
- salary_feature = salary_model_artifact.get("feature", "Age")
39
- salary_target = salary_model_artifact.get("target", "Salary")
40
- else:
41
- salary_model = salary_model_artifact
42
- salary_feature = "Age"
43
- salary_target = "Salary"
44
-
45
-
46
- # =========================
47
- # 3. Simple internal knowledge base
48
- # Replace this with your real documents later.
49
- # =========================
50
- PRODUCT_KB = """
51
- Apple Store Product Notes:
52
- - iPhone 16 is available in 128GB, 256GB, and 512GB versions.
53
- - MacBook Air is designed for light and portable productivity.
54
- - AirPods Pro supports active noise cancellation.
55
- - Promotional bundles may be offered during festive periods.
56
- """
57
-
58
- POLICY_KB = """
59
- Store Policy Notes:
60
- - Standard return period is 14 days if the item is unused and in original packaging.
61
- - Warranty coverage depends on the product category.
62
- - Shipping time depends on location and stock availability.
63
- - Refunds are processed after inspection.
64
- """
65
-
66
- TECH_KB = """
67
- Technical Support Notes:
68
- - If a phone overheats, close background apps, update software, and avoid charging during heavy use.
69
- - If Wi-Fi is unstable, restart the router and device, then forget and reconnect to the network.
70
- - If battery drains quickly, check battery health and disable unnecessary background refresh.
71
- """
72
-
73
-
74
- # =========================
75
- # 4. Helper functions
76
- # =========================
77
- def call_gemini(prompt: str) -> str:
78
- if llm is None:
79
- return (
80
- "Gemini API key is missing. In Hugging Face Spaces, add GOOGLE_API_KEY "
81
- "under Settings > Repository secrets."
82
- )
83
-
84
- try:
85
- return llm.invoke(prompt).content
86
- except Exception as e:
87
- return f"Gemini error: {e}"
88
-
89
 
90
- def predict_salary_from_text(query: str) -> str:
91
- try:
92
- dob_match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", query)
 
 
 
93
 
94
- if dob_match:
95
- dob = pd.to_datetime(dob_match.group(1), errors="coerce")
96
- if pd.isna(dob):
97
- return "Could not parse DOB. Please use YYYY-MM-DD format."
98
 
99
- today = pd.Timestamp.today()
100
- age = int((today - dob).days // 365)
101
- else:
102
- age_match = re.search(r"\b(\d{1,3})\b", query)
103
- if not age_match:
104
- return "Please provide an age or DOB, for example: 'Predict salary for age 30'."
105
- age = int(age_match.group(1))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- prediction = salary_model.predict([[age]])
108
 
109
- # Handles both scalar and nested output
110
- predicted_salary = prediction[0]
111
- if hasattr(predicted_salary, "__len__"):
112
- predicted_salary = predicted_salary[0]
 
 
 
 
 
113
 
114
- return (
115
- f"Estimated {salary_target}: {float(predicted_salary):,.2f}\n\n"
116
- f"Input used: {salary_feature} = {age}\n"
117
- f"Note: This is a model estimate, not a guaranteed salary."
118
- )
119
 
120
- except Exception as e:
121
- return f"Salary prediction error: {e}"
122
 
123
 
124
- def simple_retrieve(query: str, kb: str) -> str:
125
- # For teaching/demo: pass the relevant internal notes directly to Gemini.
126
  prompt = f"""
127
- Use the internal knowledge below to answer the user question.
128
- If the answer is not available in the internal knowledge, say that clearly.
129
 
130
- Internal knowledge:
131
- {kb}
132
 
133
- User question:
134
- {query}
135
-
136
- Answer concisely:
137
  """
138
- return call_gemini(prompt)
139
 
140
 
141
- # =========================
142
- # 5. LangGraph State
143
- # =========================
144
- class AgentState(TypedDict):
145
- query: str
146
- response: str
147
- next_node: str
148
- debug_log: str
149
-
150
 
151
- # =========================
152
- # 6. Router Node
153
- # =========================
154
- def router_node(state: AgentState):
155
- query = state["query"].lower()
156
-
157
- if any(word in query for word in ["salary", "income", "pay", "wage", "dob", "birth", "age"]):
158
- next_node = "salary_agent"
159
- elif any(word in query for word in ["return", "refund", "warranty", "shipping", "policy"]):
160
- next_node = "policy_agent"
161
- elif any(word in query for word in ["fix", "troubleshoot", "overheat", "wifi", "battery", "technical"]):
162
- next_node = "tech_agent"
163
- elif any(word in query for word in ["product", "iphone", "macbook", "airpods", "available", "buy"]):
164
- next_node = "product_agent"
165
- else:
166
- next_node = "general_agent"
167
-
168
- return {
169
- "next_node": next_node,
170
- "debug_log": f"Router selected: {next_node}"
171
- }
172
-
173
-
174
- # =========================
175
- # 7. Specialist Nodes
176
- # =========================
177
- def product_agent_node(state: AgentState):
178
- answer = simple_retrieve(state["query"], PRODUCT_KB)
179
- return {
180
- "response": answer,
181
- "debug_log": state["debug_log"] + "\nProduct Agent used internal product notes + Gemini."
182
- }
183
-
184
-
185
- def policy_agent_node(state: AgentState):
186
- answer = simple_retrieve(state["query"], POLICY_KB)
187
- return {
188
- "response": answer,
189
- "debug_log": state["debug_log"] + "\nPolicy Agent used internal policy notes + Gemini."
190
- }
191
-
192
-
193
- def tech_agent_node(state: AgentState):
194
- answer = simple_retrieve(state["query"], TECH_KB)
195
- return {
196
- "response": answer,
197
- "debug_log": state["debug_log"] + "\nTech Agent used internal technical notes + Gemini."
198
- }
199
-
200
-
201
- def salary_agent_node(state: AgentState):
202
- answer = predict_salary_from_text(state["query"])
203
- return {
204
- "response": answer,
205
- "debug_log": state["debug_log"] + "\nSalary Agent used salary_model.joblib."
206
- }
207
-
208
-
209
- def general_agent_node(state: AgentState):
210
- answer = call_gemini(state["query"])
211
- return {
212
- "response": answer,
213
- "debug_log": state["debug_log"] + "\nGeneral Agent used Gemini."
214
- }
215
-
216
-
217
- # =========================
218
- # 8. Build LangGraph
219
- # =========================
220
- workflow = StateGraph(AgentState)
221
-
222
- workflow.add_node("router", router_node)
223
- workflow.add_node("product_agent", product_agent_node)
224
- workflow.add_node("policy_agent", policy_agent_node)
225
- workflow.add_node("tech_agent", tech_agent_node)
226
- workflow.add_node("salary_agent", salary_agent_node)
227
- workflow.add_node("general_agent", general_agent_node)
228
-
229
- workflow.set_entry_point("router")
230
-
231
- workflow.add_conditional_edges(
232
- "router",
233
- lambda state: state["next_node"],
234
- {
235
- "product_agent": "product_agent",
236
- "policy_agent": "policy_agent",
237
- "tech_agent": "tech_agent",
238
- "salary_agent": "salary_agent",
239
- "general_agent": "general_agent",
240
- }
241
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- workflow.add_edge("product_agent", END)
244
- workflow.add_edge("policy_agent", END)
245
- workflow.add_edge("tech_agent", END)
246
- workflow.add_edge("salary_agent", END)
247
- workflow.add_edge("general_agent", END)
248
-
249
- graph_app = workflow.compile()
250
-
251
-
252
- # =========================
253
- # 9. Gradio UI
254
- # =========================
255
- def chat_fn(message, history):
256
- try:
257
- result = graph_app.invoke({
258
- "query": message,
259
- "response": "",
260
- "next_node": "",
261
- "debug_log": ""
262
- })
263
-
264
- answer = result.get("response", "No answer returned.")
265
- log = result.get("debug_log", "No debug log.")
266
-
267
- return f"{answer}\n\n---\nRouting trace:\n{log}"
268
-
269
- except Exception as e:
270
- return f"App error: {e}"
271
-
272
-
273
- demo = gr.ChatInterface(
274
- fn=chat_fn,
275
- title="LangGraph Multi-Agent: Gemini + Salary Prediction",
276
- description=(
277
- "Ask about products, policies, technical support, general questions, "
278
- "or salary prediction using age/DOB. DuckDuckGo has been replaced with Gemini."
279
- ),
280
- examples=[
281
- "What products are available?",
282
- "What is the return policy?",
283
- "How do I fix an overheating iPhone?",
284
- "Predict salary for age 30",
285
- "Predict salary for DOB 1995-05-20",
286
- "Explain agentic AI simply."
287
- ]
288
- )
289
 
290
  if __name__ == "__main__":
291
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
  import os
2
+ import tempfile
3
+ from typing import TypedDict, List
 
4
 
5
  import gradio as gr
 
 
 
 
6
  from langchain_google_genai import ChatGoogleGenerativeAI
7
+ from langchain_huggingface import HuggingFaceEmbeddings
8
+ from langchain_chroma import Chroma
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from langchain_core.documents import Document
11
+ from langgraph.graph import StateGraph, END
12
 
13
+ # Optional readers
14
+ from pypdf import PdfReader
15
+ import docx
16
 
17
+ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ llm = ChatGoogleGenerativeAI(
20
+ model="gemini-2.5-flash",
21
+ google_api_key=os.getenv("GOOGLE_API_KEY"),
22
+ temperature=0.2,
23
+ )
24
+ embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
25
 
26
+ VECTORSTORE = None
27
+ RETRIEVER = None
 
 
28
 
29
+ class AgentState(TypedDict):
30
+ question: str
31
+ context: str
32
+ answer: str
33
+ route: str
34
+
35
+
36
+ def read_file(path: str) -> str:
37
+ ext = os.path.splitext(path)[1].lower()
38
+ if ext == ".pdf":
39
+ reader = PdfReader(path)
40
+ return "\n".join(page.extract_text() or "" for page in reader.pages)
41
+ if ext == ".docx":
42
+ d = docx.Document(path)
43
+ return "\n".join(p.text for p in d.paragraphs)
44
+ if ext in [".txt", ".md", ".csv"]:
45
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
46
+ return f.read()
47
+ return ""
48
+
49
+
50
+ def build_rag(files):
51
+ global VECTORSTORE, RETRIEVER
52
+ if not files:
53
+ return "Please upload at least one PDF, DOCX, TXT, MD, or CSV file."
54
+
55
+ docs: List[Document] = []
56
+ for f in files:
57
+ text = read_file(f.name)
58
+ if text.strip():
59
+ docs.append(Document(page_content=text, metadata={"source": os.path.basename(f.name)}))
60
+
61
+ if not docs:
62
+ return "No readable text found. Please upload PDF, DOCX, TXT, MD, or CSV files."
63
+
64
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
65
+ chunks = splitter.split_documents(docs)
66
+
67
+ persist_dir = tempfile.mkdtemp(prefix="chroma_")
68
+ VECTORSTORE = Chroma.from_documents(
69
+ documents=chunks,
70
+ embedding=embeddings,
71
+ persist_directory=persist_dir,
72
+ collection_name="rag_collection",
73
+ )
74
+ RETRIEVER = VECTORSTORE.as_retriever(search_kwargs={"k": 4})
75
+ return f"Knowledge base ready. Loaded {len(docs)} file(s) and created {len(chunks)} chunks."
76
 
 
77
 
78
+ def retrieve_node(state: AgentState):
79
+ global RETRIEVER
80
+ if RETRIEVER is None:
81
+ return {"context": "", "route": "general"}
82
+ docs = RETRIEVER.invoke(state["question"])
83
+ context = "\n\n".join(
84
+ [f"Source: {d.metadata.get('source', 'uploaded file')}\n{d.page_content}" for d in docs]
85
+ )
86
+ return {"context": context, "route": "rag" if context.strip() else "general"}
87
 
 
 
 
 
 
88
 
89
+ def router_node(state: AgentState):
90
+ return state.get("route", "general")
91
 
92
 
93
+ def rag_answer_node(state: AgentState):
 
94
  prompt = f"""
95
+ You are a helpful RAG assistant. Answer using ONLY the context below.
96
+ If the answer is not in the context, say: "I cannot find this in the uploaded documents."
97
 
98
+ Context:
99
+ {state['context']}
100
 
101
+ Question:
102
+ {state['question']}
 
 
103
  """
104
+ return {"answer": llm.invoke(prompt).content}
105
 
106
 
107
+ def general_answer_node(state: AgentState):
108
+ prompt = f"""
109
+ The user has not built a usable knowledge base yet, or no relevant context was found.
110
+ Answer briefly and remind the user to upload documents for RAG-based answers.
 
 
 
 
 
111
 
112
+ Question: {state['question']}
113
+ """
114
+ return {"answer": llm.invoke(prompt).content}
115
+
116
+
117
+ graph = StateGraph(AgentState)
118
+ graph.add_node("retrieve", retrieve_node)
119
+ graph.add_node("rag_answer", rag_answer_node)
120
+ graph.add_node("general_answer", general_answer_node)
121
+ graph.set_entry_point("retrieve")
122
+ graph.add_conditional_edges(
123
+ "retrieve",
124
+ router_node,
125
+ {"rag": "rag_answer", "general": "general_answer"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  )
127
+ graph.add_edge("rag_answer", END)
128
+ graph.add_edge("general_answer", END)
129
+ app_graph = graph.compile()
130
+
131
+
132
+ def ask_rag(question):
133
+ if not question or not question.strip():
134
+ return "Please enter a question."
135
+ result = app_graph.invoke({"question": question, "context": "", "answer": "", "route": ""})
136
+ return result["answer"]
137
+
138
+ with gr.Blocks(title="Gemini LangGraph RAG") as demo:
139
+ gr.Markdown("# Gemini LangGraph RAG on Hugging Face")
140
+ gr.Markdown("Upload documents, build a temporary Chroma knowledge base, then ask questions.")
141
+
142
+ with gr.Row():
143
+ files = gr.File(
144
+ label="Upload documents",
145
+ file_count="multiple",
146
+ file_types=[".pdf", ".docx", ".txt", ".md", ".csv"],
147
+ )
148
+ status = gr.Textbox(label="Status")
149
 
150
+ build_btn = gr.Button("Build Knowledge Base")
151
+ build_btn.click(build_rag, inputs=files, outputs=status)
152
+
153
+ question = gr.Textbox(label="Question", placeholder="Ask about your uploaded documents")
154
+ answer = gr.Textbox(label="Answer", lines=10)
155
+ ask_btn = gr.Button("Ask RAG")
156
+ ask_btn.click(ask_rag, inputs=question, outputs=answer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  if __name__ == "__main__":
159
  demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt CHANGED
@@ -2,7 +2,9 @@ gradio
2
  langgraph
3
  langchain
4
  langchain-google-genai
5
- google-generativeai
6
- scikit-learn==1.6.1
7
- joblib
8
- pandas
 
 
 
2
  langgraph
3
  langchain
4
  langchain-google-genai
5
+ langchain-huggingface
6
+ langchain-chroma
7
+ chromadb
8
+ sentence-transformers
9
+ pypdf
10
+ python-docx