amirkiarafiei commited on
Commit
6c01c87
·
1 Parent(s): b47bb6e

init: clean up unnecessary components for demo

Browse files
.env.sample CHANGED
@@ -1,2 +1,12 @@
1
- API_KEY=
2
- MCP_SERVER_PATH=/home/andre/andre/postgre_mcp_server.py
 
 
 
 
 
 
 
 
 
 
 
1
+ MCP_SERVER_PATH=
2
+ TABLE_SUMMARY_PATH=
3
+
4
+ DB_URL=
5
+
6
+ GEMINI_API_KEY=
7
+ GEMINI_MODEL=
8
+
9
+ PANDAS_KEY=
10
+
11
+ OPENAI_MODEL=
12
+ OPENAI_API_KEY=
.gitignore CHANGED
@@ -122,4 +122,5 @@ dmypy.json
122
  .pytype/
123
 
124
  # Cython debug symbols
125
- cython_debug/
 
 
122
  .pytype/
123
 
124
  # Cython debug symbols
125
+ cython_debug/
126
+ /.gradio/
base_chat_history.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}]
chat_history.json CHANGED
@@ -1 +1 @@
1
- [{"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my previous request ?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "# Result\nlist all tables\n# Explanation\nThe user's previous request was to list all the tables in the database. This was determined by reviewing the previous turn in the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "count tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\n4\n# Explanation\nThe user requested the number of tables. The previous response listed the tables, and counting them gives the answer.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my last message ?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\ncount tables\n# Explanation\nThe user's last message was \"count tables\". This was determined by reviewing the previous turn in the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my very first message in this conversation?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nlist all tables\n# Explanation\nThe user's very first message in this conversation was \"list all tables\". This was determined by reviewing the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "total number of sales in 2024", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nThe tables in the database are:\n* dim_agreement\n* dim_customer\n* dim_product\n* dim_product_order_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}]
 
1
+ [{"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my previous request ?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "# Result\nlist all tables\n# Explanation\nThe user's previous request was to list all the tables in the database. This was determined by reviewing the previous turn in the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "count tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\n4\n# Explanation\nThe user requested the number of tables. The previous response listed the tables, and counting them gives the answer.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my last message ?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\ncount tables\n# Explanation\nThe user's last message was \"count tables\". This was determined by reviewing the previous turn in the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "what was my very first message in this conversation?", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nlist all tables\n# Explanation\nThe user's very first message in this conversation was \"list all tables\". This was determined by reviewing the conversation history.\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "total number of sales in 2024", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "list all tables", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "```markdown\n# Result\nThe tables in the database are:\n* dim_agreement\n* dim_customer\n* dim_product\n* dim_product_order_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "hi there. show me the largest table name", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "I am sorry, I cannot directly determine the \"largest table name\" based on the available tools. I can list the tables and show their schemas, but I do not have a tool to determine table sizes. I would need a tool to get table sizes to fulfill this request. However, I can list the tables for you.\n\n```markdown\n# Result\nThe tables in the database are:\n* dim\\_agreement\n* dim\\_customer\n* dim\\_product\n* dim\\_product\\_order\\_item\n\n# Explanation\nThe `list_tables` tool was called to retrieve a list of all available tables in the database schema. The result shows the names of these tables.\n\n# Query\n```sql\nN/A\n```", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}, {"type": "human", "data": {"content": "you are free to execute any sql query and get the size of each table and return me the count of largest one", "additional_kwargs": {}, "response_metadata": {}, "type": "human", "name": null, "id": null, "example": false}}, {"type": "ai", "data": {"content": "I am sorry, I cannot directly determine the \"largest table\" based on the available tools. I can list the tables and show their schemas, but I do not have a tool to determine table sizes. I would need a tool to get table sizes to fulfill this request. I can list the tables for you, but I cannot provide the count of the largest one.", "additional_kwargs": {}, "response_metadata": {}, "type": "ai", "name": null, "id": null, "example": false, "tool_calls": [], "invalid_tool_calls": [], "usage_metadata": null}}]
gradio_app.py CHANGED
@@ -2,10 +2,9 @@ import yaml
2
  from pathlib import Path
3
  import gradio as gr
4
  import asyncio
5
- from postgre_mcp_client import pg_mcp_exec
6
- from postgre_smolagent_client import pg_mcp_smolagent_exec
7
  from langchain_mcp_client import lc_mcp_exec
8
 
 
9
  def load_db_configs():
10
  """Load database configurations from configs.yaml"""
11
  configs_path = Path("configs.yaml")
@@ -18,7 +17,8 @@ def load_db_configs():
18
 
19
  return configs["db_configs"]
20
 
21
- # Async-compatible wrapper
 
22
  async def run_agent(request, history):
23
  # configs = load_db_configs()
24
  # final_answer, last_tool_answer, = await pg_mcp_exec(request)
@@ -27,6 +27,8 @@ async def run_agent(request, history):
27
  response, message = await lc_mcp_exec(request, history)
28
  return response
29
 
 
 
30
  demo = gr.ChatInterface(
31
  run_agent,
32
  type="messages",
@@ -35,6 +37,7 @@ demo = gr.ChatInterface(
35
  save_history=True,
36
  )
37
 
 
38
  # Gradio UI
39
  # demo = gr.Interface(
40
  # fn=run_agent,
@@ -53,5 +56,6 @@ demo = gr.ChatInterface(
53
  # flagging_mode="never"
54
  # )
55
 
 
56
  if __name__ == "__main__":
57
  demo.launch()
 
2
  from pathlib import Path
3
  import gradio as gr
4
  import asyncio
 
 
5
  from langchain_mcp_client import lc_mcp_exec
6
 
7
+ # ======================================= Load DB configs
8
  def load_db_configs():
9
  """Load database configurations from configs.yaml"""
10
  configs_path = Path("configs.yaml")
 
17
 
18
  return configs["db_configs"]
19
 
20
+
21
+ # ====================================== Async-compatible wrapper
22
  async def run_agent(request, history):
23
  # configs = load_db_configs()
24
  # final_answer, last_tool_answer, = await pg_mcp_exec(request)
 
27
  response, message = await lc_mcp_exec(request, history)
28
  return response
29
 
30
+
31
+ # ====================================== Gradio UI with history
32
  demo = gr.ChatInterface(
33
  run_agent,
34
  type="messages",
 
37
  save_history=True,
38
  )
39
 
40
+
41
  # Gradio UI
42
  # demo = gr.Interface(
43
  # fn=run_agent,
 
56
  # flagging_mode="never"
57
  # )
58
 
59
+
60
  if __name__ == "__main__":
61
  demo.launch()
langchain_mcp_client.py CHANGED
@@ -1,7 +1,6 @@
1
  import os.path
2
  import json
3
  from typing import Tuple, Any
4
-
5
  from mcp import ClientSession, StdioServerParameters
6
  from mcp.client.stdio import stdio_client
7
  from langchain_mcp_adapters.tools import load_mcp_tools
@@ -16,9 +15,12 @@ import logging
16
  from dotenv import load_dotenv
17
 
18
 
19
-
20
  logger = logging.getLogger(__name__)
 
 
21
  load_dotenv()
 
22
  async def lc_mcp_exec(request: str, history) -> tuple[Any, Any]:
23
  """
24
  Execute the full PostgreSQL MCP pipeline with persistent memory.
@@ -51,20 +53,21 @@ async def lc_mcp_exec(request: str, history) -> tuple[Any, Any]:
51
  elif isinstance(msg, AIMessage):
52
  formatted_history.append(AIMessage(content=msg.content))
53
 
54
- # TODO: give summary file path from config
55
- table_summary = load_table_summary("table_summary.txt")
56
  server_params = get_server_params()
57
 
58
- api_key = os.getenv("API_KEY")
59
- llm = init_chat_model(model="gemini-2.0-flash", model_provider="google_genai",
60
- api_key=api_key)
 
 
61
 
 
62
  async with stdio_client(server_params) as (read, write):
63
  async with ClientSession(read, write) as session:
64
  await session.initialize()
65
 
66
  tools = await load_and_enrich_tools(session)
67
- intent = classify_intent(request)
68
 
69
  # Add new user message before processing
70
  message_history.add_user_message(request)
@@ -85,7 +88,13 @@ async def lc_mcp_exec(request: str, history) -> tuple[Any, Any]:
85
  messages.append(HumanMessage(content=request))
86
 
87
  # Build prompt with conversation context
88
- prompt = await build_prompt(session, intent, request, tools, table_summary, formatted_history)
 
 
 
 
 
 
89
 
90
  # Invoke agent with proper message structure
91
  agent_response = await agent.ainvoke(
@@ -119,6 +128,7 @@ def load_table_summary(path: str) -> str:
119
  with open(path, 'r') as file:
120
  return file.read()
121
 
 
122
  def get_server_params() -> StdioServerParameters:
123
  # TODO: give server params from config
124
  load_dotenv()
@@ -127,41 +137,36 @@ def get_server_params() -> StdioServerParameters:
127
  args=[os.environ["MCP_SERVER_PATH"]],
128
  )
129
 
 
130
  async def load_and_enrich_tools(session: ClientSession):
131
  tools = await load_mcp_tools(session)
132
  return tools
133
 
134
- async def build_prompt(session, intent, request, tools, summary, chat_history):
135
- superset_prompt = await session.read_resource("resource://last_prompt")
136
  conversation_prompt = await session.read_resource("resource://base_prompt")
137
 
138
- if intent == "superset_request":
139
- template = superset_prompt.contents[0].text
140
- return template.format(
141
- new_request=request
142
- )
143
- else:
144
- template = conversation_prompt.contents[0].text
145
- tools_str = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools])
146
-
147
- # Handle history formatting with proper message access
148
- history_str = ""
149
- if chat_history:
150
- history_sections = []
151
- for msg in chat_history:
152
- if isinstance(msg, HumanMessage):
153
- history_sections.append(f"Previous Human Question:\n{msg.content}\n")
154
- elif isinstance(msg, AIMessage):
155
- history_sections.append(f"Previous Assistant Response:\n{msg.content}\n")
156
- history_str = "\n".join(history_sections)
157
-
158
- return template.format(
159
- new_request=request,
160
- tools=tools_str,
161
- descriptions=summary,
162
- chat_history=f"\nPrevious Conversation History:\n{history_str}" if history_str else "\nThis is a new conversation.",
163
- system_instructions="""You are a PostgreSQL database expert assistant.
164
- Use the conversation history when available to maintain context.
165
- For new conversations, focus on understanding the initial request."""
166
- )
167
 
 
1
  import os.path
2
  import json
3
  from typing import Tuple, Any
 
4
  from mcp import ClientSession, StdioServerParameters
5
  from mcp.client.stdio import stdio_client
6
  from langchain_mcp_adapters.tools import load_mcp_tools
 
15
  from dotenv import load_dotenv
16
 
17
 
18
+ # Set up logging
19
  logger = logging.getLogger(__name__)
20
+
21
+ # Load environment variables from .env file
22
  load_dotenv()
23
+
24
  async def lc_mcp_exec(request: str, history) -> tuple[Any, Any]:
25
  """
26
  Execute the full PostgreSQL MCP pipeline with persistent memory.
 
53
  elif isinstance(msg, AIMessage):
54
  formatted_history.append(AIMessage(content=msg.content))
55
 
56
+ table_summary = load_table_summary(os.environ["TABLE_SUMMARY_PATH"])
 
57
  server_params = get_server_params()
58
 
59
+ llm = init_chat_model(
60
+ model=os.getenv("GEMINI_MODEL"),
61
+ model_provider=os.getenv("GEMINI_MODEL_PROVIDER"),
62
+ api_key=os.getenv("GEMINI_API_KEY"),
63
+ )
64
 
65
+ # Initialize the MCP client
66
  async with stdio_client(server_params) as (read, write):
67
  async with ClientSession(read, write) as session:
68
  await session.initialize()
69
 
70
  tools = await load_and_enrich_tools(session)
 
71
 
72
  # Add new user message before processing
73
  message_history.add_user_message(request)
 
88
  messages.append(HumanMessage(content=request))
89
 
90
  # Build prompt with conversation context
91
+ prompt = await build_prompt(
92
+ session=session,
93
+ request=request,
94
+ tools=tools,
95
+ table_summary=table_summary,
96
+ chat_history=formatted_history
97
+ )
98
 
99
  # Invoke agent with proper message structure
100
  agent_response = await agent.ainvoke(
 
128
  with open(path, 'r') as file:
129
  return file.read()
130
 
131
+
132
  def get_server_params() -> StdioServerParameters:
133
  # TODO: give server params from config
134
  load_dotenv()
 
137
  args=[os.environ["MCP_SERVER_PATH"]],
138
  )
139
 
140
+
141
  async def load_and_enrich_tools(session: ClientSession):
142
  tools = await load_mcp_tools(session)
143
  return tools
144
 
145
+
146
+ async def build_prompt(session, request, tools, table_summary, chat_history):
147
  conversation_prompt = await session.read_resource("resource://base_prompt")
148
 
149
+ template = conversation_prompt.contents[0].text
150
+ tools_str = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools])
151
+
152
+ # Handle history formatting with proper message access
153
+ history_str = ""
154
+ if chat_history:
155
+ history_sections = []
156
+ for msg in chat_history:
157
+ if isinstance(msg, HumanMessage):
158
+ history_sections.append(f"Previous Human Question:\n{msg.content}\n")
159
+ elif isinstance(msg, AIMessage):
160
+ history_sections.append(f"Previous Assistant Response:\n{msg.content}\n")
161
+ history_str = "\n".join(history_sections)
162
+
163
+ return template.format(
164
+ new_request=request,
165
+ tools=tools_str,
166
+ descriptions=table_summary,
167
+ chat_history=f"\nPrevious Conversation History:\n{history_str}" if history_str else "\nThis is a new conversation.",
168
+ system_instructions="""You are a PostgreSQL database expert assistant.
169
+ Use the conversation history when available to maintain context.
170
+ For new conversations, focus on understanding the initial request."""
171
+ )
 
 
 
 
 
 
172
 
postgre_mcp_client.py DELETED
@@ -1,128 +0,0 @@
1
- import os.path
2
- from typing import Tuple, Any
3
-
4
- from mcp import ClientSession, StdioServerParameters
5
- from mcp.client.stdio import stdio_client
6
- from langchain_mcp_adapters.tools import load_mcp_tools
7
- from langgraph.prebuilt import create_react_agent
8
- from langchain.chat_models import init_chat_model
9
- from conversation_memory import ConversationMemory
10
- from utils import parse_mcp_output, classify_intent
11
- import logging
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- async def pg_mcp_exec(request: str) -> tuple[Any, Any]:
16
- """
17
- Execute the full PostgreSQL MCP pipeline: load summary, connect session,
18
- load memory and tools, build prompt, run agent, update memory.
19
-
20
- Args:
21
- request (str): User's request input.
22
- llm (Any): Language model for reasoning agent.
23
-
24
- Returns:
25
- str: Agent response message.
26
- """
27
- # TODO: give summary file path from config
28
- table_summary = load_table_summary("table_summary.txt")
29
- server_params = get_server_params()
30
-
31
- # TODO: give key from env
32
- llm = init_chat_model(model="gemini-2.0-flash", model_provider="google_genai",
33
- api_key="AIzaSyAuxYmci0DVU5l5L_YcxLlxHzR5MLn70js")
34
-
35
- async with stdio_client(server_params) as (read, write):
36
- async with ClientSession(read, write) as session:
37
- await session.initialize()
38
- memory = await load_or_create_memory()
39
-
40
- tools = await load_and_enrich_tools(session, table_summary)
41
- past_data = get_memory_snapshot(memory)
42
-
43
- intent = classify_intent(request)
44
- prompt = await build_prompt(session, intent, request, tools, past_data)
45
-
46
- agent = create_react_agent(llm, tools)
47
- agent_response = await agent.ainvoke({"messages": prompt})
48
-
49
- parsed_steps, final_answer, last_tool_answer, _ = parse_mcp_output(agent_response)
50
- memory.update_from_parsed(parsed_steps, request)
51
-
52
- await handle_memory_save_or_reset(memory, request)
53
-
54
- return final_answer, last_tool_answer
55
-
56
-
57
- # ---------------- Helper Functions ---------------- #
58
-
59
- def load_table_summary(path: str) -> str:
60
- with open(path, 'r') as file:
61
- return file.read()
62
-
63
- def get_server_params() -> StdioServerParameters:
64
- # TODO: give server params from config
65
- return StdioServerParameters(
66
- command="python",
67
- args=[r"/home/amirkia/Desktop/query_mcp_server/postgre_mcp_server.py"],
68
- )
69
-
70
- async def load_or_create_memory() -> ConversationMemory:
71
- memory = ConversationMemory()
72
- if os.path.exists("memory.json"):
73
- return memory.load_memory()
74
- return memory
75
-
76
- async def load_and_enrich_tools(session: ClientSession, summary: str):
77
- tools = await load_mcp_tools(session)
78
- for tool in tools:
79
- tool.description += f" {summary}"
80
- return tools
81
-
82
- def get_memory_snapshot(memory: ConversationMemory) -> dict:
83
- if os.path.exists("memory.json"):
84
- return {
85
- "past_tools": memory.get_all_tools_used(),
86
- "past_queries": memory.get_last_n_queries(),
87
- "past_results": memory.get_last_n_results(),
88
- "past_requests": memory.get_all_user_messages()
89
- }
90
- return {
91
- "past_tools": "No tools found",
92
- "past_queries": "No queries found",
93
- "past_results": "No results found",
94
- "past_requests": "No requests found"
95
- }
96
-
97
- async def build_prompt(session, intent, request, tools, past_data):
98
- superset_prompt = await session.read_resource("resource://last_prompt")
99
- conversation_prompt = await session.read_resource("resource://base_prompt")
100
- # TODO: add uri's from config
101
- if intent == "superset_request":
102
- template = superset_prompt.contents[0].text
103
- return template.format(
104
- user_requests=past_data["past_requests"],
105
- past_tools=past_data["past_tools"],
106
- last_queries=past_data["past_queries"],
107
- last_results=past_data["past_results"],
108
- new_request=request
109
- )
110
- else:
111
- template = conversation_prompt.contents[0].text
112
- tools_str = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools])
113
- return template.format(
114
- user_requests=past_data["past_requests"],
115
- past_tools=past_data["past_tools"],
116
- last_queries=past_data["past_queries"],
117
- last_results=past_data["past_results"],
118
- new_request=request,
119
- tools=tools_str
120
- )
121
-
122
- async def handle_memory_save_or_reset(memory: ConversationMemory, request: str):
123
- if request.strip().lower() == "stop":
124
- memory.reset()
125
- logger.info("Conversation memory reset.")
126
- else:
127
- memory.save_memory()
128
- logger.info("Conversation memory saved.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
postgre_mcp_server.py CHANGED
@@ -4,12 +4,12 @@ from dataclasses import dataclass
4
  from typing import Optional, AsyncIterator
5
 
6
  import asyncpg
 
7
  from mcp.server.fastmcp import FastMCP, Context
8
  from pydantic import Field
9
 
10
  # Constants
11
  DEFAULT_QUERY_LIMIT = 100
12
- DEFAULT_SCHEMA = "public"
13
 
14
 
15
  # Define our own PromptMessage class if the MCP one isn't available
@@ -31,8 +31,9 @@ class DbContext:
31
  async def db_lifespan(server: FastMCP) -> AsyncIterator[DbContext]:
32
  """Manage database connection lifecycle"""
33
  # Initialize DB connection from environment variables
34
- dsn = os.environ.get("DSN", "postgresql://postgres:postgres@localhost:5432/DNextReporting")
35
- schema = os.environ.get("SCHEMA", DEFAULT_SCHEMA)
 
36
  pool = await asyncpg.create_pool(dsn)
37
  try:
38
  yield DbContext(pool=pool, schema=schema)
@@ -58,149 +59,33 @@ async def base_prompt_query() -> str:
58
  """Returns a base prompt to generate sql queries and answer questions"""
59
 
60
  base_prompt = """
61
-
62
- ==========================
63
- # Your Role
64
- ==========================
65
-
66
- You are an expert in generating and executing SQL queries and interacting with a PostgreSQL database using **FastMCP tools**. These tools allow you to:
67
-
68
- - List available tables
69
- - Retrieve schema details
70
- - Execute SQL queries
71
-
72
-
73
- Each tool may also return previews or summaries of table contents to help you better understand the data structure.
74
-
75
- ---
76
-
77
- ==========================
78
- # Your Objective
79
- ==========================
80
-
81
- When a user submits a request, you must:
82
-
83
- 1. **Analyze the request** to determine the required data or action.
84
- 2. **Use FastMCP tools** to gather any necessary information (e.g., list tables or retrieve schema).
85
- 3. **Generate a valid SQL SELECT query**, if needed, and clearly show the full query.
86
- 4. **Execute the SQL query** and return the results.
87
- 5. **Chain tools logically**, such as: List Tables → Get Schema → Write and Run Query.
88
- 6. **Explain your reasoning and each step taken** to ensure clarity and transparency.
89
-
90
- ---
91
-
92
- ==========================
93
- # Your Objective
94
- ==========================
95
-
96
- When a user submits a request, follow these steps:
97
-
98
- 1. Analyze the request to determine the desired data or action.
99
- 2. Use tools to gather any necessary information (e.g., list tables, get schema).
100
- 3. Generate a valid SQL query (such as **SELECT**, **COUNT**, or other read-only operations) and clearly display the full query.
101
- 4. Execute the query and **return the execution result of the query**.
102
- 5. **Chain tools logically to build toward the answer.**
103
- 6. Explain your reasoning at every step for clarity and transparency.
104
- 7. Show the result of the **execute_query** in your final answer.
105
-
106
- ---
107
-
108
- ==========================
109
- # Critical Rules
110
- ==========================
111
-
112
- - Only use **read-only** SQL queries such as **SELECT**, **COUNT**, or queries with **GROUP BY**, **ORDER BY**, etc.
113
- - **Never** use destructive operations like **DELETE**, **UPDATE**, **INSERT**, or **DROP**.
114
- - Always show the SQL query you generate along with the execution result.
115
- - Validate SQL syntax before execution.
116
- - Never assume table or column names. Use tools to confirm structure.
117
- - Use memory efficiently. Don’t rerun a tool unless necessary.
118
- - If you generate a SQL query, immediately call the **execute_query** tool using that query. Do not delay or wait for user confirmation.
119
-
120
- ---
121
-
122
- ==========================
123
- # Database Description
124
- ==========================
125
-
126
- {descriptions}
127
-
128
- ---
129
-
130
- ==========================
131
- # Tools
132
- ==========================
133
-
134
- You can use the following FastMCP tools. These allow you to create **read-only** queries, such as `SELECT`, `COUNT`, or queries with `GROUP BY`, `ORDER BY`, and similar clauses. You may chain tools together to gather the necessary information before generating your SQL query.
135
-
136
- {tools}
137
-
138
- ---
139
-
140
-
141
- ### Invalid Example — DELETE Operation (Not Allowed):
142
- **User Request:** "Delete all customers from Germany."
143
-
144
- **Response Guidance:**
145
- - **Do not generate or execute** destructive queries such as `DELETE`.
146
- - Instead, respond with a message like:
147
- > Destructive operations such as `DELETE` are not permitted. I can help you retrieve the customers from Germany using a `SELECT` query instead:
148
- > ```sql
149
- > SELECT * FROM customers WHERE country = 'Germany';
150
- > ```
151
-
152
  ==========================
153
- # Output Format
154
  ==========================
155
 
156
- Present your final answer using the following structure **exactly**. When necessary, bold the important parts of your answer or use `` inline code blocks.:
157
-
158
- ```markdown
159
- # Result
160
- {{Take the result from the execute_query tool and format it nicely using Markdown. Use a Markdown table for tabular data (rows and columns) including headers. Use bullet points or items in markdown for answers that include lists of names or descriptions. Use plain text for single values or simple messages. Ensure data alignment and clarity.}}
161
-
162
- # Explanation
163
- {{Provide a concise explanation or interpretation of the results in 1-3 sentences. Explain what the data in the 'Result' section represents in the context of the user's request.}}
164
-
165
- # Query
166
- ```sql
167
- {{Display the exact SQL query you generated and executed here to answer the user's request.}}
168
 
169
- **Reminder:**
170
- **Every time you generate a SQL query, call **execute_query** right after and include the result in your final response.**
171
- **If you do not execute the generated SQL query, this will be the violation of the instructions**
172
- **Your final answer cannot be only a SQL query, you will have to call **execute_query** and give the result of the call with the SQL query.**
173
- ---
174
- {chat_history}
175
- ---
176
- =========================
177
- # New User Request
178
- =========================
179
 
180
- Please fulfill the following request based on the above context:
181
 
182
- {new_request}
183
- """
184
 
185
- return base_prompt
186
-
187
-
188
- @mcp.resource(
189
- uri="resource://last_prompt",
190
- name="last_prompt",
191
- description="A prompt that identifies the most recent SQL query related to the user's request and reformats it into ANSI SQL syntax for use in Superset."
192
- )
193
- async def last_prompt() -> str:
194
- """A prompt that identifies the most recent SQL query related to the user's request and reformats it into ANSI SQL syntax for use in Superset."""
195
-
196
- base_prompt = """
197
 
198
  ==========================
199
- # Your Role
200
  ==========================
201
 
202
- You are an expert at reading and understanding SQL queries.
203
- Your task is to retrieve the **exact SQL query** that produced a previously seen result, convert the query to the **ANSI SQL query** and return **only the ANSI SQL query** — no explanation, reasoning, or commentary.
 
 
 
 
 
 
204
 
205
  ---
206
 
@@ -208,12 +93,15 @@ Your task is to retrieve the **exact SQL query** that produced a previously seen
208
  # Your Objective
209
  ==========================
210
 
211
- When a user submits a request (e.g., *"send me that table"*, *"send the last query"*, etc.), follow these steps:
212
 
213
- 1. Identify which previous result the user is referring to.
214
- 2. Retrieve the corresponding SQL query that produced that result.
215
- 3. Convert the SQL query to the ANSI SQL query
216
- 3. Return **only** that ANSI SQL query.
 
 
 
217
 
218
  ---
219
 
@@ -221,220 +109,78 @@ When a user submits a request (e.g., *"send me that table"*, *"send the last que
221
  # Critical Rules
222
  ==========================
223
 
224
- - Do **not** ask questions or request clarification.
225
- - Do **not** explain anything to the user.
226
- - Respond with the **exact ANSI SQL query only**, formatted cleanly.
227
- - Do **not** guess only retrieve queries.
228
- - If no query fits, respond with: "Query not found."
 
 
229
 
230
  ---
231
 
232
  ==========================
233
- # Reference Conversion Rules for PostgreSQL to ANSI SQL
234
  ==========================
235
- ## Use the following rules as examples and guidance. You are not limited to these rules — apply your own knowledge of SQL standards and PostgreSQL to perform accurate conversions, even in cases not explicitly covered below.
236
-
237
- ### 1. String Concatenation
238
- - **PostgreSQL:** || for string concatenation
239
- - **ANSI SQL:** CONCAT() function
240
-
241
- **Example:**
242
- ```sql
243
- -- PostgreSQL
244
- SELECT first_name || ' ' || last_name AS full_name FROM users;
245
-
246
- -- ANSI SQL
247
- SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
248
-
249
- ### 2. LIMIT Clause
250
-
251
- - **PostgreSQL:** Uses `LIMIT`
252
- - **ANSI SQL:** Uses `FETCH FIRST n ROWS ONLY`
253
-
254
- **Example:**
255
- ```sql
256
- -- PostgreSQL
257
- SELECT * FROM posts LIMIT 10;
258
-
259
- -- ANSI SQL
260
- SELECT * FROM posts FETCH FIRST 10 ROWS ONLY;
261
-
262
- ### 3. ILIKE (Case-insensitive LIKE)
263
-
264
- - **PostgreSQL:** `ILIKE`
265
- - **ANSI SQL:** Use `LOWER(column) LIKE` or `UPPER(column) LIKE`
266
 
267
- **Example:**
268
- ```sql
269
- -- PostgreSQL
270
- SELECT * FROM users WHERE username ILIKE 'john%';
271
-
272
- -- ANSI SQL
273
- SELECT * FROM users WHERE LOWER(username) LIKE 'john%';
274
-
275
- ### 4. Type Casting
276
-
277
- - **PostgreSQL:** `'123'::INTEGER`
278
- - **ANSI SQL:** `CAST('123' AS INTEGER)`
279
-
280
- **Example:**
281
- ```sql
282
- -- PostgreSQL
283
- SELECT '123'::INTEGER;
284
-
285
- -- ANSI SQL
286
- SELECT CAST('123' AS INTEGER);
287
-
288
- ### 5. INSERT ... RETURNING
289
-
290
- - **PostgreSQL:** Supports `RETURNING` to get values immediately
291
- - **ANSI SQL:** No equivalent; use a separate `SELECT` query
292
-
293
- **Example:**
294
- ```sql
295
- -- PostgreSQL
296
- INSERT INTO users (username) VALUES ('john') RETURNING id;
297
-
298
- -- ANSI SQL
299
- -- No direct equivalent; you must SELECT after INSERT
300
- INSERT INTO users (username) VALUES ('john');
301
- SELECT id FROM users WHERE username = 'john';
302
-
303
-
304
- ### 6. UPSERT (INSERT ON CONFLICT)
305
-
306
- - **PostgreSQL:** `INSERT ... ON CONFLICT`
307
- - **ANSI SQL:** Use `MERGE` (available in ANSI SQL-2003+)
308
-
309
- **Example:**
310
- ```sql
311
- -- PostgreSQL
312
- INSERT INTO users (id, username)
313
- VALUES (1, 'john')
314
- ON CONFLICT (id)
315
- DO UPDATE SET username = EXCLUDED.username;
316
-
317
- -- ANSI SQL
318
- MERGE INTO users AS target
319
- USING (SELECT 1 AS id, 'john' AS username) AS source
320
- ON (target.id = source.id)
321
- WHEN MATCHED THEN
322
- UPDATE SET username = source.username
323
- WHEN NOT MATCHED THEN
324
- INSERT (id, username) VALUES (source.id, source.username);
325
-
326
- ### 7. SERIAL (Auto-increment IDs)
327
-
328
- - **PostgreSQL:** `SERIAL` keyword
329
- - **ANSI SQL:** Use `IDENTITY` in table creation
330
-
331
- **Example:**
332
- ```sql
333
- -- PostgreSQL
334
- CREATE TABLE users (
335
- id SERIAL PRIMARY KEY,
336
- username TEXT
337
- );
338
-
339
- -- ANSI SQL
340
- CREATE TABLE users (
341
- id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
342
- username VARCHAR(255)
343
- );
344
-
345
-
346
- ### 8. Arrays and Accessing Arrays
347
-
348
- - **PostgreSQL:** Supports arrays and uses `[index]`
349
- - **ANSI SQL:** No array type; use normalized relational design
350
-
351
- **Example:**
352
- ```sql
353
- -- PostgreSQL
354
- SELECT tags[1] FROM posts;
355
-
356
- -- ANSI SQL
357
- -- No equivalent. Use a separate `post_tags` table instead.
358
-
359
- ### 9. PostgreSQL Extensions and Functions
360
-
361
- - Replace PostgreSQL-specific functions like `NOW()`, `CURRENT_DATE`, etc., with ANSI SQL equivalents when available.
362
-
363
- **Example:**
364
- ```sql
365
- -- PostgreSQL
366
- SELECT NOW(), CURRENT_DATE;
367
-
368
- -- ANSI SQL
369
- SELECT CURRENT_TIMESTAMP, CURRENT_DATE;
370
 
371
- ### 10. Table Aliases with 'AS'
372
-
373
- - **PostgreSQL:** `FROM users AS u`
374
- - **ANSI SQL:** `FROM users u` (without `AS` is also valid)
375
-
376
- **Note:** Both are valid ANSI SQL — no change needed.
377
-
378
- ### 11. Boolean Type
379
-
380
- - **PostgreSQL:** Uses `BOOLEAN`, and accepts `TRUE` / `FALSE`
381
- - **ANSI SQL:** Also supports `BOOLEAN`, but in some systems may need conversion to `CHAR(1)` with `'Y'` / `'N'`
382
-
383
- **Example (if needed):**
384
- ```sql
385
- -- PostgreSQL
386
- SELECT * FROM flags WHERE is_enabled = TRUE;
387
 
388
- -- ANSI SQL (fallback style)
389
- SELECT * FROM flags WHERE is_enabled = 'Y';
 
390
 
 
391
 
392
- ### 12. Text vs. VARCHAR
393
 
394
- - **PostgreSQL:** Allows unbounded `TEXT`
395
- - **ANSI SQL:** Use `VARCHAR(n)`
396
 
397
- **Example:**
398
- ```sql
399
- -- PostgreSQL
400
- CREATE TABLE posts (content TEXT);
401
 
402
- -- ANSI SQL
403
- CREATE TABLE posts (content VARCHAR(65535));
404
 
405
- ---
 
 
 
 
 
 
406
 
407
  ==========================
408
- # Examples
409
  ==========================
410
 
411
- ### Example 1 Referring last query (Check the memory and find the most recent query that generates a table) :
412
- **User Request:** "send the last table"
413
- **You return:**
414
- ```sql
415
- SELECT * FROM posts INNER JOIN users ON posts.user_id = users.id;
416
 
417
- ### Example 2 — Referring to a specific query (check the memory and find the query that returns the count of users):
418
- **User Request:** "send the query that gave us the count of users"
419
- **You return:**
420
- ```sql
421
- SELECT COUNT(*) FROM users;
422
 
423
- ### Example 3 — Referring latest known query (Check the memory and find the most recent query.)
424
- **User Request:** "send"
425
- **You return:**
426
- (latest known query):
427
- ```sql
428
- SELECT * FROM posts WHERE user_id = 1;
429
 
430
- Remember: Only respond with valid SQL from memory converted to the ANSI SQL. No assumptions. No explanations.
 
 
431
 
 
 
 
 
 
 
 
432
  =========================
433
  # New User Request
434
  =========================
435
- {new_request}
436
 
437
- """
 
 
 
438
 
439
  return base_prompt
440
 
@@ -565,7 +311,7 @@ async def list_tables() -> str:
565
  async def get_table_schema(table_name: str) -> str:
566
  """Get schema information for a specific table"""
567
  try:
568
- schema = os.environ.get("SCHEMA", DEFAULT_SCHEMA)
569
 
570
  async with db_lifespan(mcp) as db_ctx:
571
  columns = await get_table_schema_info(db_ctx.pool, schema, table_name)
@@ -588,7 +334,7 @@ def get_foreign_keys(table_name: str) -> str:
588
  table_name: The name of the table to get foreign keys from
589
  schema: The schema name (defaults to 'public')
590
  """
591
- schema = os.environ.get("SCHEMA", DEFAULT_SCHEMA)
592
 
593
  sql = """
594
  SELECT
@@ -617,7 +363,7 @@ def get_foreign_keys(table_name: str) -> str:
617
  async def get_all_schemas() -> str:
618
  """Get schema information for all tables in the database"""
619
  try:
620
- schema = os.environ.get("SCHEMA", DEFAULT_SCHEMA)
621
 
622
  async with db_lifespan(mcp) as db_ctx:
623
  tables = await get_all_tables(db_ctx.pool, db_ctx.schema)
 
4
  from typing import Optional, AsyncIterator
5
 
6
  import asyncpg
7
+ from flask.cli import load_dotenv
8
  from mcp.server.fastmcp import FastMCP, Context
9
  from pydantic import Field
10
 
11
  # Constants
12
  DEFAULT_QUERY_LIMIT = 100
 
13
 
14
 
15
  # Define our own PromptMessage class if the MCP one isn't available
 
31
  async def db_lifespan(server: FastMCP) -> AsyncIterator[DbContext]:
32
  """Manage database connection lifecycle"""
33
  # Initialize DB connection from environment variables
34
+ load_dotenv()
35
+ dsn = os.getenv("DB_URL")
36
+ schema = os.getenv("DB_SCHEMA")
37
  pool = await asyncpg.create_pool(dsn)
38
  try:
39
  yield DbContext(pool=pool, schema=schema)
 
59
  """Returns a base prompt to generate sql queries and answer questions"""
60
 
61
  base_prompt = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  ==========================
63
+ # Your Role
64
  ==========================
65
 
66
+ You are an expert in generating and executing SQL queries and interacting with a PostgreSQL database using **FastMCP tools**. These tools allow you to:
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ - List available tables
69
+ - Retrieve schema details
70
+ - Execute SQL queries
 
 
 
 
 
 
 
71
 
 
72
 
73
+ Each tool may also return previews or summaries of table contents to help you better understand the data structure.
 
74
 
75
+ ---
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  ==========================
78
+ # Your Objective
79
  ==========================
80
 
81
+ When a user submits a request, you must:
82
+
83
+ 1. **Analyze the request** to determine the required data or action.
84
+ 2. **Use FastMCP tools** to gather any necessary information (e.g., list tables or retrieve schema).
85
+ 3. **Generate a valid SQL SELECT query**, if needed, and clearly show the full query.
86
+ 4. **Execute the SQL query** and return the results.
87
+ 5. **Chain tools logically**, such as: List Tables → Get Schema → Write and Run Query.
88
+ 6. **Explain your reasoning and each step taken** to ensure clarity and transparency.
89
 
90
  ---
91
 
 
93
  # Your Objective
94
  ==========================
95
 
96
+ When a user submits a request, follow these steps:
97
 
98
+ 1. Analyze the request to determine the desired data or action.
99
+ 2. Use tools to gather any necessary information (e.g., list tables, get schema).
100
+ 3. Generate a valid SQL query (such as **SELECT**, **COUNT**, or other read-only operations) and clearly display the full query.
101
+ 4. Execute the query and **return the execution result of the query**.
102
+ 5. **Chain tools logically to build toward the answer.**
103
+ 6. Explain your reasoning at every step for clarity and transparency.
104
+ 7. Show the result of the **execute_query** in your final answer.
105
 
106
  ---
107
 
 
109
  # Critical Rules
110
  ==========================
111
 
112
+ - Only use **read-only** SQL queries such as **SELECT**, **COUNT**, or queries with **GROUP BY**, **ORDER BY**, etc.
113
+ - **Never** use destructive operations like **DELETE**, **UPDATE**, **INSERT**, or **DROP**.
114
+ - Always show the SQL query you generate along with the execution result.
115
+ - Validate SQL syntax before execution.
116
+ - Never assume table or column names. Use tools to confirm structure.
117
+ - Use memory efficiently. Don’t rerun a tool unless necessary.
118
+ - If you generate a SQL query, immediately call the **execute_query** tool using that query. Do not delay or wait for user confirmation.
119
 
120
  ---
121
 
122
  ==========================
123
+ # Database Description
124
  ==========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ {descriptions}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ ==========================
131
+ # Tools
132
+ ==========================
133
 
134
+ You can use the following FastMCP tools. These allow you to create **read-only** queries, such as `SELECT`, `COUNT`, or queries with `GROUP BY`, `ORDER BY`, and similar clauses. You may chain tools together to gather the necessary information before generating your SQL query.
135
 
136
+ {tools}
137
 
138
+ ---
 
139
 
 
 
 
 
140
 
141
+ ### Invalid Example — DELETE Operation (Not Allowed):
142
+ **User Request:** "Delete all customers from Germany."
143
 
144
+ **Response Guidance:**
145
+ - **Do not generate or execute** destructive queries such as `DELETE`.
146
+ - Instead, respond with a message like:
147
+ > Destructive operations such as `DELETE` are not permitted. I can help you retrieve the customers from Germany using a `SELECT` query instead:
148
+ > ```sql
149
+ > SELECT * FROM customers WHERE country = 'Germany';
150
+ > ```
151
 
152
  ==========================
153
+ # Output Format
154
  ==========================
155
 
156
+ Present your final answer using the following structure **exactly**. When necessary, bold the important parts of your answer or use `` inline code blocks.:
 
 
 
 
157
 
158
+ ```markdown
159
+ # Result
160
+ {{Take the result from the execute_query tool and format it nicely using Markdown. Use a Markdown table for tabular data (rows and columns) including headers. Use bullet points or items in markdown for answers that include lists of names or descriptions. Use plain text for single values or simple messages. Ensure data alignment and clarity.}}
 
 
161
 
162
+ # Explanation
163
+ {{Provide a concise explanation or interpretation of the results in 1-3 sentences. Explain what the data in the 'Result' section represents in the context of the user's request.}}
 
 
 
 
164
 
165
+ # Query
166
+ ```sql
167
+ {{Display the exact SQL query you generated and executed here to answer the user's request.}}
168
 
169
+ **Reminder:**
170
+ **Every time you generate a SQL query, call **execute_query** right after and include the result in your final response.**
171
+ **If you do not execute the generated SQL query, this will be the violation of the instructions**
172
+ **Your final answer cannot be only a SQL query, you will have to call **execute_query** and give the result of the call with the SQL query.**
173
+ ---
174
+ {chat_history}
175
+ ---
176
  =========================
177
  # New User Request
178
  =========================
 
179
 
180
+ Please fulfill the following request based on the above context:
181
+
182
+ {new_request}
183
+ """
184
 
185
  return base_prompt
186
 
 
311
  async def get_table_schema(table_name: str) -> str:
312
  """Get schema information for a specific table"""
313
  try:
314
+ schema = os.getenv("DB_SCHEMA")
315
 
316
  async with db_lifespan(mcp) as db_ctx:
317
  columns = await get_table_schema_info(db_ctx.pool, schema, table_name)
 
334
  table_name: The name of the table to get foreign keys from
335
  schema: The schema name (defaults to 'public')
336
  """
337
+ schema = os.getenv("DB_SCHEMA")
338
 
339
  sql = """
340
  SELECT
 
363
  async def get_all_schemas() -> str:
364
  """Get schema information for all tables in the database"""
365
  try:
366
+ schema = os.getenv("DB_SCHEMA")
367
 
368
  async with db_lifespan(mcp) as db_ctx:
369
  tables = await get_all_tables(db_ctx.pool, db_ctx.schema)
postgre_smolagent_client.py DELETED
@@ -1,133 +0,0 @@
1
- import os.path
2
- from mcp import ClientSession, StdioServerParameters
3
- from mcp.client.stdio import stdio_client
4
- from langchain_mcp_adapters.tools import load_mcp_tools
5
- from langgraph.prebuilt import create_react_agent
6
- from langchain.chat_models import init_chat_model
7
- from conversation_memory import ConversationMemory
8
- from utils import parse_mcp_output, classify_intent
9
- import logging
10
- from smolagents import LiteLLMModel, ToolCollection, CodeAgent
11
- from dotenv import load_dotenv
12
-
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- async def pg_mcp_smolagent_exec(request: str) -> str:
17
- """
18
- Execute the full PostgreSQL MCP pipeline: load summary, connect session,
19
- load memory and tools, build prompt, run agent, update memory.
20
-
21
- Args:
22
- request (str): User's request input.
23
- llm (Any): Language model for reasoning agent.
24
-
25
- Returns:
26
- str: Agent response message.
27
- """
28
- # TODO: give summary file path from config
29
- table_summary = load_table_summary("table_summary.txt")
30
- server_params = get_server_params()
31
-
32
- # TODO: give key from env
33
- llm = LiteLLMModel(model_id="gemini/gemini-2.0-flash", api_key="AIzaSyDRoQ8yG0Rmpt3NtfHS7fdm5hgfL9su5aY")
34
-
35
- async with stdio_client(server_params) as (read, write):
36
- async with ClientSession(read, write) as session:
37
- await session.initialize()
38
- memory = await load_or_create_memory()
39
-
40
- intent = classify_intent(request)
41
- with ToolCollection.from_mcp(server_params, trust_remote_code=True) as tool_collection:
42
- # Create a Code agent using the LLM and tools
43
- agent = CodeAgent(model=llm, tools=[*tool_collection.tools], add_base_tools=True)
44
- tools = await load_and_enrich_tools(session, table_summary)
45
- past_data = get_memory_snapshot(memory)
46
-
47
- prompt = await build_prompt(session, intent, request, tools, past_data, table_summary)
48
- agent_response = agent.run(task=prompt, stream=False)
49
-
50
- # TODO: add a smolagent output parser
51
-
52
- #parsed_steps, _ = parse_mcp_output(agent_response)
53
- #memory.update_from_parsed(parsed_steps, request)
54
-
55
- #await handle_memory_save_or_reset(memory, request)
56
-
57
- return agent_response
58
-
59
-
60
- # ---------------- Helper Functions ---------------- #
61
-
62
- def load_table_summary(path: str) -> str:
63
- with open(path, 'r') as file:
64
- return file.read()
65
-
66
- def get_server_params() -> StdioServerParameters:
67
- # TODO: give server params from config
68
- load_dotenv()
69
- return StdioServerParameters(
70
- command="python",
71
- args=[os.environ["MCP_SERVER_PATH"]],
72
- )
73
-
74
- async def load_or_create_memory() -> ConversationMemory:
75
- memory = ConversationMemory()
76
- if os.path.exists("memory.json"):
77
- return memory.load_memory()
78
- return memory
79
-
80
- async def load_and_enrich_tools(session: ClientSession, summary: str):
81
- tools = await load_mcp_tools(session)
82
- #for tool in tools:
83
- # tool.description += f" {summary}"
84
- return tools
85
-
86
- def get_memory_snapshot(memory: ConversationMemory) -> dict:
87
- if os.path.exists("memory.json"):
88
- return {
89
- "past_tools": memory.get_all_tools_used(),
90
- "past_queries": memory.get_last_n_queries(),
91
- "past_results": memory.get_last_n_results(),
92
- "past_requests": memory.get_all_user_messages()
93
- }
94
- return {
95
- "past_tools": "No tools found",
96
- "past_queries": "No queries found",
97
- "past_results": "No results found",
98
- "past_requests": "No requests found"
99
- }
100
-
101
- async def build_prompt(session, intent, request, tools, past_data, summary):
102
- superset_prompt = await session.read_resource("resource://last_prompt")
103
- conversation_prompt = await session.read_resource("resource://base_prompt")
104
- # TODO: add uri's from config
105
- if intent == "superset_request":
106
- template = superset_prompt.contents[0].text
107
- return template.format(
108
- user_requests=past_data["past_requests"],
109
- past_tools=past_data["past_tools"],
110
- last_queries=past_data["past_queries"],
111
- last_results=past_data["past_results"],
112
- new_request=request
113
- )
114
- else:
115
- template = conversation_prompt.contents[0].text
116
- tools_str = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools])
117
- return template.format(
118
- user_requests=past_data["past_requests"],
119
- past_tools=past_data["past_tools"],
120
- last_queries=past_data["past_queries"],
121
- last_results=past_data["past_results"],
122
- new_request=request,
123
- tools=tools_str,
124
- descriptions=summary
125
- )
126
-
127
- async def handle_memory_save_or_reset(memory: ConversationMemory, request: str):
128
- if request.strip().lower() == "stop":
129
- memory.reset()
130
- logger.info("Conversation memory reset.")
131
- else:
132
- memory.save_memory()
133
- logger.info("Conversation memory saved.")