Rajesh Betkiker commited on
Commit
0321eee
Β·
1 Parent(s): 7422798

Added tools

Browse files
.gitignore ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.13
README.md CHANGED
@@ -1,14 +1,46 @@
1
  ---
2
- title: Mcp Hackathon
3
- emoji: πŸ‘
4
  colorFrom: green
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.32.1
8
  app_file: app.py
9
  pinned: false
10
- license: apache-2.0
11
- short_description: Develop Model Context Protocol (MCP) server with Gradio App
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MCP - RAG and Research
3
+ emoji: 🌍
4
  colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 5.32.1
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Demonstrates implementation of the MCP server using Gradio
12
+ tag: mcp-server-track
13
  ---
14
 
15
+ # πŸ‘ MCP powered RAG and Research 🌍
16
+
17
+ I present to you a MCP powered RAG and Research.
18
+
19
+ RAG Tool uses GroundX service to fetch the knowledge base. The knowledge base is a document that contains information about the SU-35 aircraft, including its features, capabilities, and specifications.
20
+ Please check [this PDF](https://airgroup2000.com/gallery/albums/userpics/32438/SU-35_TM_eng.pdf) to formulate queries on Sukhoi.
21
+
22
+ The Research Tool is implemented using multi-agent workflow using LlamaIndex (ResearchAgent, WriteAgent, and ReviewAgent).
23
+
24
+ ## Available Tools
25
+
26
+ ### search_knowledge_base_for_context
27
+ - **Description**: Searches and retrieves relevant context from a knowledge base based on the user's query.
28
+ - **Example Queries**:
29
+ - "What are the main features of fuel system of SU-35."
30
+ - "What are the combat potential of SU-35."
31
+
32
+ ### research_write_review_topic
33
+ - **Description**: Helps with writing a report with research, writing, and review on any topic.
34
+ - **Example Queries**:
35
+ - "Write me a report on the history of the internet."
36
+ - "Write me a report on origin of the universe."
37
+ - "Write me a report on the impact of climate change on polar bears."
38
+
39
+ ## How to Use
40
+ - Use the MCP RAG Tool tab above to query the knowledge base.
41
+ - Use the Research Tool tab above to write report on any topic.
42
+
43
+ ## Demo Link
44
+ [Link to Demo on Youtube](https://www.youtube.com/mcp-rag-research)
45
+
46
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
mcp_client.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This is only a sample code snippet for a Gradio interface that connects to an MCP server. Server URL is set to local, it won't work on spaces.
3
+
4
+ This script initializes a Gradio interface for an agent that uses tools from the MCP server.
5
+ It connects to the MCP server, retrieves available tools, and sets up a chat interface where users can interact with the agent.
6
+ """
7
+
8
+ import os
9
+ from dotenv import load_dotenv
10
+ load_dotenv() # Load environment variables from .env file
11
+
12
+ import gradio as gr
13
+ from smolagents import InferenceClientModel, CodeAgent, MCPClient
14
+
15
+ try:
16
+ mcp_client = MCPClient(
17
+ {
18
+ "url": "http://localhost:7860/gradio_api/mcp/sse",
19
+ "transport": "sse"
20
+ }
21
+ )
22
+
23
+ tools = mcp_client.get_tools()
24
+
25
+ model = InferenceClientModel(token=os.getenv("HUGGINGFACE_API_TOKEN"))
26
+ agent = CodeAgent(tools=[*tools], model=model)
27
+
28
+ demo = gr.ChatInterface(
29
+ fn=lambda message, history: str(agent.run(message)),
30
+ type="messages",
31
+ title="Agent with MCP Tools",
32
+ description="This is a simple MCP Client build with Gradio that uses MCP tools.",
33
+ )
34
+
35
+ demo.launch()
36
+ finally:
37
+ mcp_client.disconnect()
pyproject.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "mcp-rag-workflow"
3
+ version = "0.1.0"
4
+ description = "Model Context Protocol (MCP) server with RAG and Research Tools"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "gradio[mcp]",
9
+ "smolagents[mcp]",
10
+ "llama-index",
11
+ "llama-index-llms-nebius",
12
+ "groundx",
13
+ "duckduckgo-search",
14
+ "langchain-community>=0.3.24",
15
+ ]
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio[mcp]
2
+ smolagents[mcp]
3
+ llama-index
4
+ llama-index-llms-nebius
5
+ groundx
6
+ duckduckgo-search
7
+ langchain-community>=0.3.24
tools/multi_agent_workflow_for_research.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This file contains the multi-agent workflow for the research project.
3
+ Using LlamaIndex built a modular, intelligent multi-agent workflow.
4
+ With real-time tools and structured memory.
5
+
6
+ The workflow is as follows:
7
+
8
+ 1. The ResearchAgent searches the web for information.
9
+ 2. The WriteAgent writes a report based on the research notes.
10
+ 3. The ReviewAgent reviews the report and provides feedback.
11
+
12
+ """
13
+
14
+ import os
15
+ import asyncio
16
+
17
+ # Load environment variables from .env file
18
+ from dotenv import load_dotenv
19
+ load_dotenv()
20
+
21
+ from llama_index.llms.nebius import NebiusLLM
22
+
23
+ # llama-index workflow classes
24
+ from llama_index.core.workflow import Context
25
+ from llama_index.core.agent.workflow import (
26
+ FunctionAgent,
27
+ AgentWorkflow,
28
+ AgentOutput,
29
+ ToolCall,
30
+ ToolCallResult,
31
+ )
32
+
33
+ from langchain.utilities import DuckDuckGoSearchAPIWrapper
34
+
35
+ NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY")
36
+
37
+ # Load an LLM
38
+ llm = NebiusLLM(
39
+ api_key=NEBIUS_API_KEY,
40
+ model="meta-llama/Meta-Llama-3.1-8B-Instruct",
41
+ is_function_calling_model=True
42
+ )
43
+
44
+ # Search tools using DuckDuckGo
45
+ duckduckgo = DuckDuckGoSearchAPIWrapper()
46
+
47
+ MAX_SEARCH_CALLS = 2 # Limit the number of searches to 2
48
+ search_call_count = 0
49
+ past_queries = set()
50
+
51
+ async def safe_duckduckgo_search(query: str) -> str:
52
+ """
53
+ A DuckDuckGo-based search function that:
54
+ - Prevents more than MAX_SEARCH_CALLS total searches.
55
+ - Skips duplicate queries.
56
+ """
57
+ global search_call_count, past_queries
58
+
59
+ # Check for duplicate queries
60
+ if query in past_queries:
61
+ return f"Already searched for '{query}'. Avoiding duplicate search."
62
+
63
+ # Check if we've reached the max search calls
64
+ if search_call_count >= MAX_SEARCH_CALLS:
65
+ return "Search limit reached, no more searches allowed."
66
+
67
+ # Otherwise, perform the search
68
+ search_call_count += 1
69
+ past_queries.add(query)
70
+
71
+ # DuckDuckGoSearchAPIWrapper.run(...) is synchronous, but we have an async signature
72
+ result = duckduckgo.run(query)
73
+ return str(result)
74
+
75
+ # Research tools
76
+ async def save_research(ctx: Context, notes: str, notes_title: str) -> str:
77
+ """
78
+ Store research notes under a given title in the shared context.
79
+ """
80
+
81
+ current_state = await ctx.get("state")
82
+ if "research_notes" not in current_state:
83
+ current_state["research_notes"] = {}
84
+ current_state["research_notes"][notes_title] = notes
85
+ await ctx.set("state", current_state)
86
+ return "Notes saved."
87
+
88
+ # Report tools
89
+ async def write_report(ctx: Context, report_content: str) -> str:
90
+ """
91
+ Write a report in markdown, storing it in the shared context.
92
+ """
93
+
94
+ current_state = await ctx.get("state")
95
+ current_state["report_content"] = report_content
96
+ await ctx.set("state", current_state)
97
+ return "Report written."
98
+
99
+ # Review tools
100
+ async def review_report(ctx: Context, review: str) -> str:
101
+ """
102
+ Review the report and store feedback in the shared context.
103
+ """
104
+
105
+ current_state = await ctx.get("state")
106
+ current_state["review"] = review
107
+ await ctx.set("state", current_state)
108
+ return "Report reviewed."
109
+
110
+
111
+ # We have three agents with distinct responsibilities:
112
+ # - The ResearchAgent is responsible for gathering information from the web.
113
+ # - The WriteAgent is responsible for writing the report.
114
+ # - The ReviewAgent is responsible for reviewing the report.
115
+
116
+ # The ResearchAgent uses the DuckDuckGoSearchAPIWrapper to search the web.
117
+
118
+ research_agent = FunctionAgent(
119
+ name="ResearchAgent",
120
+ description=(
121
+ "A research agent that searches the web using Google search through SerpAPI. "
122
+ "It must not exceed 2 searches total, and must avoid repeating the same query. "
123
+ "Once sufficient information is collected, it should hand off to the WriteAgent."
124
+ ),
125
+ system_prompt=(
126
+ "You are the ResearchAgent. Your goal is to gather sufficient information on the topic. "
127
+ "Only perform at most 2 distinct searches. If you have enough information or have reached 2 searches, "
128
+ "handoff to the WriteAgent. Avoid infinite loops! If search throws an error, stop further work and skip WriteAgent and ReviewAgent and return."
129
+ "Respect invocation limits and cooldown periods."
130
+ ),
131
+ llm=llm,
132
+ tools=[
133
+ safe_duckduckgo_search,
134
+ save_research
135
+ ],
136
+ max_iterations=2, # Limit to 2 iterations to prevent infinite loops
137
+ cooldown=5, # Cooldown to prevent rapid re-querying
138
+ can_handoff_to=["WriteAgent"]
139
+ )
140
+
141
+ write_agent = FunctionAgent(
142
+ name="WriteAgent",
143
+ description=(
144
+ "Writes a markdown report based on the research notes. "
145
+ "Then hands off to the ReviewAgent for feedback."
146
+ ),
147
+ system_prompt=(
148
+ "You are the WriteAgent. Draft a structured markdown report based on the notes. "
149
+ "If there is no report content or research notes, stop further work and skip ReviewAgent."
150
+ "Do not attempt more than one write attempt. "
151
+ "After writing, hand off to the ReviewAgent."
152
+ "Respect invocation limits and cooldown periods."
153
+ ),
154
+ llm=llm,
155
+ tools=[write_report],
156
+ max_iterations=2, # Limit to 2 iterations to prevent infinite loops
157
+ cooldown=5, # Cooldown to prevent rapid re-querying
158
+ can_handoff_to=["ReviewAgent", "ResearchAgent"]
159
+ )
160
+
161
+ review_agent = FunctionAgent(
162
+ name="ReviewAgent",
163
+ description=(
164
+ "Reviews the final report for correctness. Approves or requests changes."
165
+ ),
166
+ system_prompt=(
167
+ "You are the ReviewAgent. If there is no research notes or report content, skip this step and return."
168
+ "Do not attempt more than one review attempt. "
169
+ "Read the report, provide feedback, and either approve "
170
+ "or request revisions. If revisions are needed, handoff to WriteAgent."
171
+ "Respect invocation limits and cooldown periods."
172
+ ),
173
+ llm=llm,
174
+ tools=[review_report],
175
+ max_iterations=2, # Limit to 2 iterations to prevent infinite loops
176
+ cooldown=5, # Cooldown to prevent rapid re-querying
177
+ can_handoff_to=["WriteAgent"]
178
+ )
179
+
180
+ agent_workflow = AgentWorkflow(
181
+ agents=[research_agent, write_agent, review_agent],
182
+ root_agent=research_agent.name, # Start with the ResearchAgent
183
+ initial_state={
184
+ "research_notes": {},
185
+ "report_content": "Not written yet.",
186
+ "review": "Review required.",
187
+ },
188
+ )
189
+
190
+ async def execute_research_workflow(query: str):
191
+ handler = agent_workflow.run(
192
+ user_msg=(
193
+ query
194
+ )
195
+ )
196
+
197
+ current_agent = None
198
+
199
+ async for event in handler.stream_events():
200
+ if hasattr(event, "current_agent_name") and event.current_agent_name != current_agent:
201
+ current_agent = event.current_agent_name
202
+ print(f"\n{'='*50}")
203
+ print(f"πŸ€– Agent: {current_agent}")
204
+ print(f"{'='*50}\n")
205
+
206
+ # Print outputs or tool calls
207
+ if isinstance(event, AgentOutput):
208
+ if event.response.content:
209
+ print("πŸ“€ Output:", event.response.content)
210
+ if event.tool_calls:
211
+ print("πŸ› οΈ Planning to use tools:", [call.tool_name for call in event.tool_calls])
212
+
213
+ elif isinstance(event, ToolCall):
214
+ print(f"πŸ”¨ Calling Tool: {event.tool_name}")
215
+ print(f" With arguments: {event.tool_kwargs}")
216
+
217
+ elif isinstance(event, ToolCallResult):
218
+ print(f"πŸ”§ Tool Result ({event.tool_name}):")
219
+ print(f" Arguments: {event.tool_kwargs}")
220
+ print(f" Output: {event.tool_output}")
221
+
222
+ return handler
223
+
224
+ async def final_report(handler) -> str:
225
+ """Retrieve the final report from the context."""
226
+ final_state = await handler.ctx.get("state")
227
+ print("\n\n=============================")
228
+ print("FINAL REPORT:\n")
229
+ print(final_state["report_content"])
230
+ print("=============================\n")
231
+
232
+ return final_state["report_content"]
233
+
234
+ def run_research_workflow(query: str):
235
+ handler = asyncio.run(execute_research_workflow(query))
236
+ result = asyncio.run(final_report(handler))
237
+ return result
tools/rag_tools.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ This file contains the tools for the RAG workflow.
3
+ """
4
+
5
+ import os
6
+ from groundx import GroundX
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ client = GroundX(api_key=os.getenv("GROUNDX_API_KEY") or '')
12
+
13
+ def search_groundx_for_rag_context(query: str) -> str:
14
+ """
15
+ Searches and retrieves relevant context from a knowledge base,
16
+ based on the user's query.
17
+ Args:
18
+ query: The search query supplied by the user.
19
+ Returns:
20
+ str: Relevant text content that can be used by the LLM to answer the query.
21
+ """
22
+
23
+ response = client.search.content(
24
+ id=os.getenv("GROUNDX_BUCKET_ID"),
25
+ query=query,
26
+ n=10,
27
+ )
28
+
29
+ return response.search.text or "No relevant context found"
uv.lock ADDED
The diff for this file is too large to render. See raw diff