timeki commited on
Commit
073a9d2
·
verified ·
1 Parent(s): 9b4cec0

Upload 2 files

Browse files
Files changed (2) hide show
  1. mcp_service.py +103 -0
  2. scripts/mcp_client.py +273 -0
mcp_service.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from climateqa.engine.chains.retrieve_documents import retrieve_documents
3
+ from climateqa.engine.graph_retriever import retrieve_graphs
4
+
5
+
6
+ def make_retrieve_data_mcp(vectorstore, reranker):
7
+ """
8
+ MCP-exposed function: retrieve IPx (IPCC/IPBES/IPOS) documents and figures.
9
+ """
10
+ async def retrieve_data_mcp(
11
+ query: str,
12
+ ) -> str:
13
+ """
14
+ MCP-exposed function: retrieve IPx (IPCC/IPBES/IPOS) documents and figures.
15
+
16
+ Args:
17
+ query (str): The user query.
18
+
19
+ Returns:
20
+ str: JSON with keys: "documents", "images".
21
+ """
22
+
23
+ current_question = {
24
+ "question": query,
25
+ "sources": ["IPCC", "IPBES", "IPOS"],
26
+ "source_type": "IPx",
27
+ "index": 1,
28
+ }
29
+
30
+ docs, images = await retrieve_documents(
31
+ current_question=current_question,
32
+ config={"logging": True},
33
+ source_type="IPx",
34
+ vectorstore=vectorstore,
35
+ reranker=reranker,
36
+ search_figures=True,
37
+ rerank_by_question=True,
38
+ k_images_by_question=5,
39
+ k_before_reranking=100,
40
+ k_by_question=5,
41
+ k_summary_by_question=3,
42
+ )
43
+
44
+ def _serialize(docs_list):
45
+ return [
46
+ {
47
+ "content": getattr(d, "page_content", ""),
48
+ "metadata": getattr(d, "metadata", {}),
49
+ }
50
+ for d in (docs_list or [])
51
+ ]
52
+
53
+ payload = {
54
+ "documents": _serialize(docs),
55
+ "images": _serialize(images),
56
+ }
57
+
58
+ return json.dumps(payload)
59
+
60
+ return retrieve_data_mcp
61
+
62
+
63
+ def make_retrieve_graphs_mcp(vectorstore):
64
+ """
65
+ MCP-exposed function: retrieve graphs (OWID).
66
+ """
67
+ async def retrieve_graphs_mcp(
68
+ query: str,
69
+ ) -> str:
70
+ """
71
+ MCP-exposed function: retrieve graphs (OWID).
72
+
73
+ Args:
74
+ query (str): The user query.
75
+
76
+ Returns:
77
+ str: JSON with key: "graphs".
78
+ """
79
+
80
+ docs = await retrieve_graphs(
81
+ query=query,
82
+ vectorstore=vectorstore,
83
+ # sources=["OWID"],
84
+ threshold=0.2, # Lowered from 0.5 to get more results
85
+ k_total=10,
86
+ )
87
+
88
+ def _serialize(docs_list):
89
+ return [
90
+ {
91
+ "content": getattr(d, "page_content", ""),
92
+ "metadata": getattr(d, "metadata", {}),
93
+ }
94
+ for d in (docs_list or [])
95
+ ]
96
+
97
+ payload = {
98
+ "graphs": _serialize(docs),
99
+ }
100
+
101
+ return json.dumps(payload)
102
+
103
+ return retrieve_graphs_mcp
scripts/mcp_client.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ ClimateQA MCP Client - Test and interact with the ClimateQA MCP server.
4
+
5
+ This script demonstrates how to connect to the ClimateQA MCP server using
6
+ the OpenAI Agents SDK and query climate-related documents and graphs.
7
+
8
+ Usage:
9
+ # List available MCP tools
10
+ python scripts/mcp_client.py list-tools
11
+
12
+ # Run a single query
13
+ python scripts/mcp_client.py query "What causes climate change?"
14
+
15
+ # Interactive chat mode
16
+ python scripts/mcp_client.py interactive
17
+
18
+ # Custom MCP server URL
19
+ python scripts/mcp_client.py --url http://localhost:7960/gradio_api/mcp/sse query "..."
20
+
21
+ Requirements:
22
+ pip install openai-agents
23
+
24
+ Environment:
25
+ OPENAI_API_KEY: Required for the agent
26
+ MCP_SERVER_URL: Optional, defaults to http://localhost:7960/gradio_api/mcp/sse
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import asyncio
33
+ import json
34
+ import os
35
+ import sys
36
+ from typing import TYPE_CHECKING
37
+
38
+ # Load environment variables
39
+ try:
40
+ from dotenv import load_dotenv
41
+ load_dotenv()
42
+ except ImportError:
43
+ pass
44
+
45
+ if TYPE_CHECKING:
46
+ from agents import Agent
47
+ from agents.mcp import MCPServerSse
48
+
49
+ # Configuration
50
+ DEFAULT_MCP_URL = "http://localhost:7960/gradio_api/mcp/sse"
51
+ DEFAULT_MODEL = "gpt-4o-mini"
52
+ TOOL_RESULT_PREVIEW_LENGTH = 500
53
+
54
+
55
+ def get_mcp_url() -> str:
56
+ """Get the MCP server URL from environment or default."""
57
+ return os.getenv("MCP_SERVER_URL", DEFAULT_MCP_URL)
58
+
59
+
60
+ def check_api_key() -> None:
61
+ """Verify OpenAI API key is set."""
62
+ if not os.getenv("OPENAI_API_KEY"):
63
+ print("❌ Error: OPENAI_API_KEY environment variable is required")
64
+ print(" Set it with: export OPENAI_API_KEY='your-key'")
65
+ sys.exit(1)
66
+
67
+
68
+ def create_mcp_server(url: str) -> "MCPServerSse":
69
+ """Create an MCP server connection."""
70
+ from agents.mcp import MCPServerSse
71
+
72
+ return MCPServerSse(
73
+ params={"url": url},
74
+ name="climateqa",
75
+ cache_tools_list=True,
76
+ )
77
+
78
+
79
+ def create_agent(mcp_server: "MCPServerSse") -> "Agent":
80
+ """Create the ClimateQA agent with MCP tools."""
81
+ from agents import Agent
82
+
83
+ return Agent(
84
+ name="ClimateQA Agent",
85
+ instructions="""You are a climate research assistant with access to scientific
86
+ documents from IPCC, IPBES, IPOS reports and graphs from IEA and OWID.
87
+
88
+ When answering climate-related questions:
89
+ 1. Use retrieve_data_mcp to get relevant documents and figures from climate reports
90
+ 2. Use retrieve_graphs_mcp to get relevant data visualizations
91
+ 3. Synthesize the information into a clear, well-sourced answer
92
+
93
+ Always cite your sources and mention which reports the information comes from.""",
94
+ mcp_servers=[mcp_server],
95
+ model=DEFAULT_MODEL,
96
+ )
97
+
98
+
99
+ async def list_tools(url: str) -> None:
100
+ """List all available MCP tools from the server."""
101
+ print(f"\n📡 Connecting to: {url}")
102
+ print("=" * 60)
103
+
104
+ mcp_server = create_mcp_server(url)
105
+
106
+ async with mcp_server:
107
+ tools = await mcp_server.list_tools()
108
+
109
+ if not tools:
110
+ print("⚠️ No tools found on this MCP server")
111
+ return
112
+
113
+ print(f"Found {len(tools)} tool(s):\n")
114
+
115
+ for tool in tools:
116
+ print(f"📌 {tool.name}")
117
+ if tool.description:
118
+ print(f" {tool.description}")
119
+ if tool.inputSchema:
120
+ schema = json.dumps(tool.inputSchema, indent=2)
121
+ print(f" Schema: {schema}")
122
+ print()
123
+
124
+
125
+ async def run_query(url: str, query: str) -> None:
126
+ """Run a single query through the agent."""
127
+ from agents import Runner
128
+
129
+ check_api_key()
130
+
131
+ print(f"\n📡 MCP Server: {url}")
132
+ print(f"❓ Query: {query}")
133
+ print("=" * 60)
134
+
135
+ mcp_server = create_mcp_server(url)
136
+ agent = create_agent(mcp_server)
137
+
138
+ async with mcp_server:
139
+ result = Runner.run_streamed(agent, query)
140
+
141
+ async for event in result.stream_events():
142
+ if event.type == "run_item_stream_event":
143
+ item = event.item
144
+ item_type = getattr(item, "type", None)
145
+
146
+ if item_type == "tool_call_item":
147
+ name = getattr(item, "name", "unknown")
148
+ args = getattr(item, "arguments", "{}")
149
+ print(f"\n🔧 Calling: {name}")
150
+ print(f" Args: {_truncate(str(args), 200)}")
151
+
152
+ elif item_type == "tool_call_output_item":
153
+ output = getattr(item, "output", "")
154
+ print(f"\n📥 Result preview:")
155
+ print(f" {_truncate(str(output), TOOL_RESULT_PREVIEW_LENGTH)}")
156
+
157
+ print("\n" + "=" * 60)
158
+ print("🤖 Agent Response:")
159
+ print("=" * 60)
160
+ print(result.final_output)
161
+
162
+
163
+ async def interactive_mode(url: str) -> None:
164
+ """Run the agent in interactive chat mode."""
165
+ from agents import Runner
166
+
167
+ check_api_key()
168
+
169
+ print("\n" + "=" * 60)
170
+ print("🌍 ClimateQA MCP Agent - Interactive Mode")
171
+ print("=" * 60)
172
+ print(f"📡 Server: {url}")
173
+ print("💡 Type your questions (or 'quit' to exit)")
174
+ print("=" * 60)
175
+
176
+ mcp_server = create_mcp_server(url)
177
+ agent = create_agent(mcp_server)
178
+
179
+ async with mcp_server:
180
+ while True:
181
+ try:
182
+ query = input("\n❓ You: ").strip()
183
+
184
+ if query.lower() in ("quit", "exit", "q"):
185
+ print("👋 Goodbye!")
186
+ break
187
+
188
+ if not query:
189
+ continue
190
+
191
+ print("\n⏳ Thinking...")
192
+
193
+ result = Runner.run_streamed(agent, query)
194
+
195
+ async for event in result.stream_events():
196
+ if event.type == "run_item_stream_event":
197
+ item = event.item
198
+ item_type = getattr(item, "type", None)
199
+
200
+ if item_type == "tool_call_item":
201
+ name = getattr(item, "name", "unknown")
202
+ print(f" 🔧 Using: {name}")
203
+
204
+ elif item_type == "tool_call_output_item":
205
+ output = getattr(item, "output", "")
206
+ print(f" 📥 Got {len(str(output))} chars")
207
+
208
+ print(f"\n🤖 Agent: {result.final_output}")
209
+
210
+ except KeyboardInterrupt:
211
+ print("\n\n👋 Interrupted. Goodbye!")
212
+ break
213
+ except Exception as e:
214
+ print(f"\n❌ Error: {e}")
215
+
216
+
217
+ def _truncate(text: str, length: int) -> str:
218
+ """Truncate text with ellipsis if too long."""
219
+ if len(text) <= length:
220
+ return text
221
+ return text[:length] + "..."
222
+
223
+
224
+ def main() -> None:
225
+ """Main entry point."""
226
+ parser = argparse.ArgumentParser(
227
+ description="ClimateQA MCP Client - Query climate documents via MCP",
228
+ formatter_class=argparse.RawDescriptionHelpFormatter,
229
+ epilog="""
230
+ Examples:
231
+ %(prog)s list-tools # List available MCP tools
232
+ %(prog)s query "What causes global warming?" # Run a single query
233
+ %(prog)s interactive # Interactive chat mode
234
+ %(prog)s --url http://host:7960/... query .. # Use custom server URL
235
+ """,
236
+ )
237
+
238
+ parser.add_argument(
239
+ "--url",
240
+ type=str,
241
+ default=None,
242
+ help=f"MCP server URL (default: {DEFAULT_MCP_URL})",
243
+ )
244
+
245
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
246
+
247
+ # list-tools command
248
+ subparsers.add_parser("list-tools", help="List available MCP tools")
249
+
250
+ # query command
251
+ query_parser = subparsers.add_parser("query", help="Run a single query")
252
+ query_parser.add_argument("text", type=str, help="The question to ask")
253
+
254
+ # interactive command
255
+ subparsers.add_parser("interactive", help="Interactive chat mode")
256
+
257
+ args = parser.parse_args()
258
+
259
+ # Determine URL
260
+ url = args.url or get_mcp_url()
261
+
262
+ if args.command == "list-tools":
263
+ asyncio.run(list_tools(url))
264
+ elif args.command == "query":
265
+ asyncio.run(run_query(url, args.text))
266
+ elif args.command == "interactive":
267
+ asyncio.run(interactive_mode(url))
268
+ else:
269
+ parser.print_help()
270
+
271
+
272
+ if __name__ == "__main__":
273
+ main()