Spaces:
Runtime error
Runtime error
Abhishek commited on
Commit ·
e086837
1
Parent(s): 5369e8c
Added dummy sqlite DB
Browse files- .dummyenv +11 -1
- .gitignore +0 -1
- .gradio/certificate.pem +31 -0
- README.md +5 -0
- app.py +303 -33
- core/mcp_tools/erp_db_init.py +145 -0
- core/mcp_tools/erp_server.py +264 -116
- core/mcp_tools/global_disruptions_server.py +138 -28
- core/mcp_tools/populate_erp_db.py +415 -0
- core/mcp_tools/reset_erp_db.py +47 -0
- data/erp_db.sqlite +0 -0
.dummyenv
CHANGED
|
@@ -4,9 +4,19 @@ OPENAI_API_KEY=""
|
|
| 4 |
# Weather API Key
|
| 5 |
WEATHER_API_KEY = ""
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
# PostgreSQL Database Configuration
|
| 8 |
POSTGRES_HOST = ""
|
| 9 |
POSTGRES_DB = ""
|
| 10 |
POSTGRES_USER = ""
|
| 11 |
POSTGRES_PASSWORD = ""
|
| 12 |
-
POSTGRES_PORT = "5432"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
# Weather API Key
|
| 5 |
WEATHER_API_KEY = ""
|
| 6 |
|
| 7 |
+
# Database Configuration
|
| 8 |
+
ERP_DB_TYPE = "sqlite" # Options: "sqlite" or "postgres"
|
| 9 |
+
SQLITE_DB_PATH = "./data/erp_db.sqlite"
|
| 10 |
+
|
| 11 |
# PostgreSQL Database Configuration
|
| 12 |
POSTGRES_HOST = ""
|
| 13 |
POSTGRES_DB = ""
|
| 14 |
POSTGRES_USER = ""
|
| 15 |
POSTGRES_PASSWORD = ""
|
| 16 |
+
POSTGRES_PORT = "5432"
|
| 17 |
+
|
| 18 |
+
# ERP Postgres DB
|
| 19 |
+
ERP_DB_TYPE = "postgres" # Options: "postgres" or "sqlite"
|
| 20 |
+
|
| 21 |
+
# Disruption DB
|
| 22 |
+
DISRUPTIONS_DB_TYPE = "postgres" # Options: "postgres" or "sqlite"
|
.gitignore
CHANGED
|
@@ -195,5 +195,4 @@ cython_debug/
|
|
| 195 |
.env
|
| 196 |
.vscode
|
| 197 |
|
| 198 |
-
data
|
| 199 |
notebooks
|
|
|
|
| 195 |
.env
|
| 196 |
.vscode
|
| 197 |
|
|
|
|
| 198 |
notebooks
|
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
README.md
CHANGED
|
@@ -21,6 +21,11 @@ tags:
|
|
| 21 |
|
| 22 |
**TrackMate AI** is a Gradio-powered autonomous agent designed to interface with an ERP system via MCP tools. It connects to a custom MCP Server and lets users chat with an LLM to place, modify, or track orders — all through natural language.
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
---
|
| 25 |
|
| 26 |
## 📺 Demo Video
|
|
|
|
| 21 |
|
| 22 |
**TrackMate AI** is a Gradio-powered autonomous agent designed to interface with an ERP system via MCP tools. It connects to a custom MCP Server and lets users chat with an LLM to place, modify, or track orders — all through natural language.
|
| 23 |
|
| 24 |
+
Designed for seamless interaction with ERP infrastructure, TrackMate AI empowers users to manage operational workflows via a conversational interface — no dashboards, no dropdowns, just intelligent dialogue.
|
| 25 |
+
|
| 26 |
+
Whether you're tracking an order, generating invoices, or accessing global risk intelligence, TrackMate AI brings an intuitive, natural language layer to your business operations.
|
| 27 |
+
|
| 28 |
+
|
| 29 |
---
|
| 30 |
|
| 31 |
## 📺 Demo Video
|
app.py
CHANGED
|
@@ -89,49 +89,319 @@ def reset_conversation():
|
|
| 89 |
|
| 90 |
# ----- Gradio Interface -----
|
| 91 |
def create_interface():
|
| 92 |
-
with gr.Blocks(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
agent_state = gr.State(value=None)
|
| 94 |
-
|
|
|
|
| 95 |
gr.HTML("""
|
| 96 |
-
<div
|
| 97 |
-
<
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
</div>
|
| 100 |
""")
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
| 102 |
with gr.Column(scale=1):
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
value="",
|
| 112 |
-
label="API Key",
|
| 113 |
-
placeholder="Enter your OpenAI API key",
|
| 114 |
-
type="password",
|
| 115 |
-
interactive=True
|
| 116 |
-
)
|
| 117 |
-
with gr.Accordion("System Prompt", open=False):
|
| 118 |
-
system_prompt = gr.Textbox(
|
| 119 |
-
value=get_default_system_prompt(),
|
| 120 |
-
lines=10,
|
| 121 |
-
label="System Prompt",
|
| 122 |
-
interactive=True
|
| 123 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
|
|
|
| 129 |
with gr.Column(scale=3, min_width=700):
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
|
|
|
| 135 |
init_btn.click(
|
| 136 |
fn=initialize_agent_handler,
|
| 137 |
inputs=[model_name, temperature, max_tokens, system_prompt, api_key],
|
|
|
|
| 89 |
|
| 90 |
# ----- Gradio Interface -----
|
| 91 |
def create_interface():
|
| 92 |
+
with gr.Blocks(
|
| 93 |
+
title="TrackMate AI",
|
| 94 |
+
theme=gr.themes.Soft(),
|
| 95 |
+
css="""
|
| 96 |
+
/* Dark theme colors */
|
| 97 |
+
:root {
|
| 98 |
+
--bg-primary: #121212;
|
| 99 |
+
--bg-secondary: #1e1e1e;
|
| 100 |
+
--bg-tertiary: #252525;
|
| 101 |
+
--text-primary: #e0e0e0;
|
| 102 |
+
--text-secondary: #a0a0a0;
|
| 103 |
+
--accent-primary: #3a506b;
|
| 104 |
+
--accent-secondary: #1c2541;
|
| 105 |
+
--accent-tertiary: #0b132b;
|
| 106 |
+
--highlight: #5bc0be;
|
| 107 |
+
--error: #ff6b6b;
|
| 108 |
+
--success: #6bd425;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.documentation-container {
|
| 112 |
+
background-color: var(--bg-secondary);
|
| 113 |
+
color: var(--text-primary);
|
| 114 |
+
padding: 2rem;
|
| 115 |
+
border-radius: 15px;
|
| 116 |
+
margin-bottom: 2rem;
|
| 117 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.documentation-header {
|
| 121 |
+
text-align: center;
|
| 122 |
+
margin-bottom: 1.5rem;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.documentation-section {
|
| 126 |
+
background-color: var(--bg-tertiary);
|
| 127 |
+
padding: 1.5rem;
|
| 128 |
+
border-radius: 10px;
|
| 129 |
+
margin-bottom: 1.5rem;
|
| 130 |
+
border-left: 4px solid var(--highlight);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.config-panel {
|
| 134 |
+
background-color: var(--bg-secondary);
|
| 135 |
+
padding: 1.5rem;
|
| 136 |
+
border-radius: 15px;
|
| 137 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 138 |
+
height: 100%;
|
| 139 |
+
display: flex;
|
| 140 |
+
flex-direction: column;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.chat-panel {
|
| 144 |
+
background-color: var(--bg-secondary);
|
| 145 |
+
padding: 1.5rem;
|
| 146 |
+
border-radius: 15px;
|
| 147 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 148 |
+
height: 100%;
|
| 149 |
+
display: flex;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.panel-header {
|
| 154 |
+
text-align: center;
|
| 155 |
+
padding: 1rem;
|
| 156 |
+
background-color: var(--accent-primary);
|
| 157 |
+
color: var(--text-primary);
|
| 158 |
+
border-radius: 10px;
|
| 159 |
+
margin-bottom: 1rem;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.tools-table {
|
| 163 |
+
width: 100%;
|
| 164 |
+
border-collapse: collapse;
|
| 165 |
+
margin-top: 1rem;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.tools-table th, .tools-table td {
|
| 169 |
+
padding: 0.75rem;
|
| 170 |
+
text-align: left;
|
| 171 |
+
border-bottom: 1px solid var(--accent-secondary);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.tools-table th {
|
| 175 |
+
font-weight: bold;
|
| 176 |
+
background-color: var(--accent-tertiary);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.feature-list {
|
| 180 |
+
display: grid;
|
| 181 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 182 |
+
gap: 0.5rem;
|
| 183 |
+
margin-top: 1rem;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.feature-item {
|
| 187 |
+
background-color: var(--accent-secondary);
|
| 188 |
+
padding: 0.75rem;
|
| 189 |
+
border-radius: 10px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
a {
|
| 193 |
+
color: var(--highlight);
|
| 194 |
+
text-decoration: none;
|
| 195 |
+
font-weight: bold;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
a:hover {
|
| 199 |
+
text-decoration: underline;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* Chat avatar styling */
|
| 203 |
+
.chatbot .avatar {
|
| 204 |
+
display: flex !important;
|
| 205 |
+
align-items: center;
|
| 206 |
+
justify-content: center;
|
| 207 |
+
width: 40px !important;
|
| 208 |
+
height: 40px !important;
|
| 209 |
+
border-radius: 50%;
|
| 210 |
+
background-color: var(--accent-secondary);
|
| 211 |
+
color: var(--text-primary);
|
| 212 |
+
font-size: 20px;
|
| 213 |
+
margin-right: 10px;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Ensure chat messages have proper contrast */
|
| 217 |
+
.chatbot .user-message {
|
| 218 |
+
background-color: var(--accent-primary) !important;
|
| 219 |
+
color: var(--text-primary) !important;
|
| 220 |
+
border-radius: 10px !important;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.chatbot .bot-message {
|
| 224 |
+
background-color: var(--accent-secondary) !important;
|
| 225 |
+
color: var(--text-primary) !important;
|
| 226 |
+
border-radius: 10px !important;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/* Override any theme styles that might hide avatars */
|
| 230 |
+
.chatbot .message-container {
|
| 231 |
+
display: flex !important;
|
| 232 |
+
align-items: flex-start !important;
|
| 233 |
+
}
|
| 234 |
+
"""
|
| 235 |
+
) as app:
|
| 236 |
agent_state = gr.State(value=None)
|
| 237 |
+
|
| 238 |
+
# Consolidated Documentation Section
|
| 239 |
gr.HTML("""
|
| 240 |
+
<div class="documentation-container">
|
| 241 |
+
<div class="documentation-header">
|
| 242 |
+
<h1 style="margin: 0; font-size: 3rem; font-weight: 700;">🤖 TrackMate AI</h1>
|
| 243 |
+
<p style="margin: 0.5rem 0 0 0; font-size: 1.2rem; opacity: 0.9;">Your smart companion for tracking and managing orders.</p>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<div class="documentation-section">
|
| 247 |
+
<h2 style="margin: 0 0 1rem 0; font-size: 1.8rem;">📋 About TrackMate AI</h2>
|
| 248 |
+
<p style="margin: 0; font-size: 1.1rem; line-height: 1.6;">
|
| 249 |
+
<strong>TrackMate AI</strong> is a Gradio-powered autonomous agent designed to interface with an ERP system via MCP tools.
|
| 250 |
+
It connects to a custom MCP Server and lets users chat with an LLM to place, modify, or track orders — all through natural language. Designed for seamless interaction with ERP infrastructure, TrackMate AI empowers users to manage operational workflows via a conversational interface — no dashboards, no dropdowns, just intelligent dialogue. Whether you're tracking an order, generating invoices, or accessing global risk intelligence, TrackMate AI brings an intuitive, natural language layer to your business operations.
|
| 251 |
+
</p>
|
| 252 |
+
|
| 253 |
+
<h3 style="margin: 1.5rem 0 0.5rem 0; font-size: 1.4rem;">📺 Demo Video</h3>
|
| 254 |
+
<p style="margin: 0; font-size: 1.1rem;">
|
| 255 |
+
<a href="https://youtu.be/hzaNbPRrrz8" target="_blank">
|
| 256 |
+
📹 Watch Demo Video - TrackMate AI Agent
|
| 257 |
+
</a>
|
| 258 |
+
</p>
|
| 259 |
+
|
| 260 |
+
<h3 style="margin: 1.5rem 0 0.5rem 0; font-size: 1.4rem;">✨ What It Can Do</h3>
|
| 261 |
+
<div class="feature-list">
|
| 262 |
+
<div class="feature-item">🔍 Query live ERP data (orders, invoices, customers)</div>
|
| 263 |
+
<div class="feature-item">�� Place or cancel orders</div>
|
| 264 |
+
<div class="feature-item">🌐 Check for geopolitical disruptions</div>
|
| 265 |
+
<div class="feature-item">🧠 Leverage multiple tools via MCP + Gradio agent framework</div>
|
| 266 |
+
<div class="feature-item">🧮 Use a calculator for quick computations</div>
|
| 267 |
+
<div class="feature-item">☀️🌧️ Get live weather updates for any location</div>
|
| 268 |
+
</div>
|
| 269 |
+
|
| 270 |
+
<h3 style="margin: 1.5rem 0 0.5rem 0; font-size: 1.4rem;">🔌 Connected Tools</h3>
|
| 271 |
+
<p style="margin: 0 0 1rem 0; font-size: 1.1rem;">
|
| 272 |
+
TrackMate AI uses a bunch of tools provided by the
|
| 273 |
+
<a href="https://huggingface.co/spaces/Agents-MCP-Hackathon/TrackMate-AI-MCP-Server" target="_blank">
|
| 274 |
+
TrackMate AI MCP Server
|
| 275 |
+
</a>:
|
| 276 |
+
</p>
|
| 277 |
+
<table style="width: 100%; border-collapse: collapse; border: 1px solid white;">
|
| 278 |
+
<thead>
|
| 279 |
+
<tr>
|
| 280 |
+
<th style="border: 1px solid white; padding: 8px; background-color: #333; color: white;">Tool Name</th>
|
| 281 |
+
<th style="border: 1px solid white; padding: 8px; background-color: #333; color: white;">Description</th>
|
| 282 |
+
</tr>
|
| 283 |
+
</thead>
|
| 284 |
+
<tbody>
|
| 285 |
+
<tr>
|
| 286 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>list_tables</code></td>
|
| 287 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Lists all ERP database tables</td>
|
| 288 |
+
</tr>
|
| 289 |
+
<tr>
|
| 290 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>execute_query</code></td>
|
| 291 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Executes a custom SQL query</td>
|
| 292 |
+
</tr>
|
| 293 |
+
<tr>
|
| 294 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>get_order_status</code></td>
|
| 295 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Gets status of an order</td>
|
| 296 |
+
</tr>
|
| 297 |
+
<tr>
|
| 298 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>place_order</code></td>
|
| 299 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Places a new order</td>
|
| 300 |
+
</tr>
|
| 301 |
+
<tr>
|
| 302 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>cancel_order</code></td>
|
| 303 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Cancels an existing order</td>
|
| 304 |
+
</tr>
|
| 305 |
+
<tr>
|
| 306 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>get_active_disruptions</code></td>
|
| 307 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Returns geopolitical disruptions between nations</td>
|
| 308 |
+
</tr>
|
| 309 |
+
<tr>
|
| 310 |
+
<td style="border: 1px solid white; padding: 8px; color: white;"><code>add, subtract, multiple</code></td>
|
| 311 |
+
<td style="border: 1px solid white; padding: 8px; color: white;">Use a simple calculator</td>
|
| 312 |
+
</tr>
|
| 313 |
+
</tbody>
|
| 314 |
+
</table>
|
| 315 |
+
|
| 316 |
+
</div>
|
| 317 |
</div>
|
| 318 |
""")
|
| 319 |
+
|
| 320 |
+
# Main Interface
|
| 321 |
+
with gr.Row(equal_height=True):
|
| 322 |
+
# Configuration Panel
|
| 323 |
with gr.Column(scale=1):
|
| 324 |
+
with gr.Group(elem_classes="config-panel"):
|
| 325 |
+
gr.HTML('<div class="panel-header"><h3 style="margin: 0;">⚙️ Agent Configuration</h3></div>')
|
| 326 |
+
|
| 327 |
+
model_name = gr.Dropdown(
|
| 328 |
+
choices=["gpt-4o-mini", "gpt-4o", "gpt-4"],
|
| 329 |
+
value="gpt-4o",
|
| 330 |
+
label="🤖 Model",
|
| 331 |
+
info="Select the OpenAI model to use"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
)
|
| 333 |
+
temperature = gr.Slider(
|
| 334 |
+
0.0, 1.0,
|
| 335 |
+
value=0.7,
|
| 336 |
+
step=0.1,
|
| 337 |
+
label="🌡️ Temperature",
|
| 338 |
+
info="Controls randomness (0=deterministic, 1=creative)"
|
| 339 |
+
)
|
| 340 |
+
max_tokens = gr.Number(
|
| 341 |
+
value=1000,
|
| 342 |
+
minimum=100,
|
| 343 |
+
maximum=4000,
|
| 344 |
+
step=100,
|
| 345 |
+
label="📏 Max Tokens",
|
| 346 |
+
info="Maximum response length"
|
| 347 |
+
)
|
| 348 |
+
api_key = gr.Textbox(
|
| 349 |
+
value="",
|
| 350 |
+
label="🔑 API Key",
|
| 351 |
+
placeholder="Enter your OpenAI API key",
|
| 352 |
+
type="password",
|
| 353 |
+
interactive=True,
|
| 354 |
+
info="Your OpenAI API key (kept secure)"
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
with gr.Accordion("📝 System Prompt", open=False):
|
| 358 |
+
system_prompt = gr.Textbox(
|
| 359 |
+
value=get_default_system_prompt(),
|
| 360 |
+
lines=10,
|
| 361 |
+
label="System Prompt",
|
| 362 |
+
interactive=True,
|
| 363 |
+
info="Customize the agent's behavior and instructions"
|
| 364 |
+
)
|
| 365 |
|
| 366 |
+
init_btn = gr.Button("🚀 Initialize Agent", variant="primary", size="lg")
|
| 367 |
+
|
| 368 |
+
with gr.Group():
|
| 369 |
+
status_display = gr.Textbox(
|
| 370 |
+
label="📊 Status",
|
| 371 |
+
interactive=False,
|
| 372 |
+
info="Current operation status"
|
| 373 |
+
)
|
| 374 |
+
agent_status = gr.Textbox(
|
| 375 |
+
label="🤖 Agent Status",
|
| 376 |
+
interactive=False,
|
| 377 |
+
info="Agent readiness indicator"
|
| 378 |
+
)
|
| 379 |
|
| 380 |
+
# Chat Panel
|
| 381 |
with gr.Column(scale=3, min_width=700):
|
| 382 |
+
with gr.Group(elem_classes="chat-panel"):
|
| 383 |
+
gr.HTML('<div class="panel-header"><h3 style="margin: 0;">💬 Chat Interface</h3></div>')
|
| 384 |
+
|
| 385 |
+
chatbot = gr.Chatbot(
|
| 386 |
+
label="TrackMate AI Chat",
|
| 387 |
+
height=650,
|
| 388 |
+
show_label=False,
|
| 389 |
+
show_copy_button=True,
|
| 390 |
+
elem_classes="chatbot"
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Make the message input take full width
|
| 394 |
+
msg_input = gr.Textbox(
|
| 395 |
+
label="💭 Message",
|
| 396 |
+
placeholder="Ask me about orders, inventory, weather, or anything else...",
|
| 397 |
+
show_label=False
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
send_btn = gr.Button("📤 Send", variant="primary", scale=1)
|
| 401 |
+
|
| 402 |
+
reset_btn = gr.Button("🔄 Reset Chat", variant="secondary", scale=1)
|
| 403 |
|
| 404 |
+
# Event handlers
|
| 405 |
init_btn.click(
|
| 406 |
fn=initialize_agent_handler,
|
| 407 |
inputs=[model_name, temperature, max_tokens, system_prompt, api_key],
|
core/mcp_tools/erp_db_init.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# erp_db_init.py
|
| 2 |
+
import sqlite3
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
|
| 6 |
+
def init_sqlite_db(db_path='./data/erp_db.sqlite'):
|
| 7 |
+
"""Initialize SQLite database with ERP tables."""
|
| 8 |
+
try:
|
| 9 |
+
# Create the database directory if it doesn't exist
|
| 10 |
+
db_dir = pathlib.Path(os.path.dirname(db_path))
|
| 11 |
+
db_dir.mkdir(exist_ok=True)
|
| 12 |
+
|
| 13 |
+
# Connect to SQLite database (will be created if it doesn't exist)
|
| 14 |
+
conn = sqlite3.connect(db_path)
|
| 15 |
+
cursor = conn.cursor()
|
| 16 |
+
|
| 17 |
+
# Enable foreign keys
|
| 18 |
+
conn.execute("PRAGMA foreign_keys = ON")
|
| 19 |
+
|
| 20 |
+
# Create customers table
|
| 21 |
+
cursor.execute('''
|
| 22 |
+
CREATE TABLE IF NOT EXISTS erp_customers (
|
| 23 |
+
customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 24 |
+
name TEXT NOT NULL,
|
| 25 |
+
email TEXT NOT NULL UNIQUE,
|
| 26 |
+
phone TEXT,
|
| 27 |
+
address TEXT,
|
| 28 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 29 |
+
)
|
| 30 |
+
''')
|
| 31 |
+
|
| 32 |
+
# Create products table
|
| 33 |
+
cursor.execute('''
|
| 34 |
+
CREATE TABLE IF NOT EXISTS erp_products (
|
| 35 |
+
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 36 |
+
product_name TEXT NOT NULL,
|
| 37 |
+
description TEXT,
|
| 38 |
+
category TEXT,
|
| 39 |
+
price REAL NOT NULL,
|
| 40 |
+
stock_quantity INTEGER NOT NULL DEFAULT 0,
|
| 41 |
+
sku TEXT UNIQUE
|
| 42 |
+
)
|
| 43 |
+
''')
|
| 44 |
+
|
| 45 |
+
# Create orders table
|
| 46 |
+
cursor.execute('''
|
| 47 |
+
CREATE TABLE IF NOT EXISTS erp_orders (
|
| 48 |
+
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 49 |
+
customer_id INTEGER,
|
| 50 |
+
order_date DATE DEFAULT CURRENT_DATE,
|
| 51 |
+
total_amount REAL NOT NULL,
|
| 52 |
+
status TEXT NOT NULL DEFAULT 'Processing',
|
| 53 |
+
previous_order_id INTEGER,
|
| 54 |
+
estimated_delivery DATE,
|
| 55 |
+
actual_delivery DATE,
|
| 56 |
+
payment_status TEXT DEFAULT 'Pending',
|
| 57 |
+
shipping_address TEXT NOT NULL,
|
| 58 |
+
shipping_country TEXT,
|
| 59 |
+
destination_country TEXT,
|
| 60 |
+
FOREIGN KEY (customer_id) REFERENCES erp_customers (customer_id),
|
| 61 |
+
FOREIGN KEY (previous_order_id) REFERENCES erp_orders (order_id)
|
| 62 |
+
)
|
| 63 |
+
''')
|
| 64 |
+
|
| 65 |
+
# Create order items table
|
| 66 |
+
cursor.execute('''
|
| 67 |
+
CREATE TABLE IF NOT EXISTS erp_order_items (
|
| 68 |
+
order_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 69 |
+
order_id INTEGER,
|
| 70 |
+
product_id INTEGER,
|
| 71 |
+
quantity INTEGER NOT NULL,
|
| 72 |
+
unit_price REAL NOT NULL,
|
| 73 |
+
subtotal REAL NOT NULL,
|
| 74 |
+
FOREIGN KEY (order_id) REFERENCES erp_orders (order_id),
|
| 75 |
+
FOREIGN KEY (product_id) REFERENCES erp_products (product_id)
|
| 76 |
+
)
|
| 77 |
+
''')
|
| 78 |
+
|
| 79 |
+
# Create order history table
|
| 80 |
+
cursor.execute('''
|
| 81 |
+
CREATE TABLE IF NOT EXISTS erp_order_history (
|
| 82 |
+
history_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 83 |
+
order_id INTEGER,
|
| 84 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 85 |
+
status_change TEXT,
|
| 86 |
+
notes TEXT,
|
| 87 |
+
updated_by TEXT,
|
| 88 |
+
FOREIGN KEY (order_id) REFERENCES erp_orders (order_id)
|
| 89 |
+
)
|
| 90 |
+
''')
|
| 91 |
+
|
| 92 |
+
# Create invoices table
|
| 93 |
+
cursor.execute('''
|
| 94 |
+
CREATE TABLE IF NOT EXISTS erp_invoices (
|
| 95 |
+
invoice_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 96 |
+
order_id INTEGER,
|
| 97 |
+
invoice_date DATE DEFAULT CURRENT_DATE,
|
| 98 |
+
amount REAL NOT NULL,
|
| 99 |
+
payment_terms TEXT,
|
| 100 |
+
due_date DATE,
|
| 101 |
+
is_paid BOOLEAN DEFAULT 0,
|
| 102 |
+
invoice_number TEXT UNIQUE,
|
| 103 |
+
FOREIGN KEY (order_id) REFERENCES erp_orders (order_id)
|
| 104 |
+
)
|
| 105 |
+
''')
|
| 106 |
+
|
| 107 |
+
# Create global disruptions table
|
| 108 |
+
cursor.execute('''
|
| 109 |
+
CREATE TABLE IF NOT EXISTS live_global_disruptions (
|
| 110 |
+
disruption_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 111 |
+
source_country TEXT NOT NULL,
|
| 112 |
+
destination_country TEXT NOT NULL,
|
| 113 |
+
disruption_type TEXT NOT NULL,
|
| 114 |
+
severity INTEGER NOT NULL CHECK (severity >= 1 AND severity <= 5),
|
| 115 |
+
start_date DATE NOT NULL,
|
| 116 |
+
expected_end_date DATE,
|
| 117 |
+
actual_end_date DATE,
|
| 118 |
+
is_active BOOLEAN DEFAULT 1,
|
| 119 |
+
description TEXT,
|
| 120 |
+
impact_hours INTEGER,
|
| 121 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 122 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 123 |
+
UNIQUE (source_country, destination_country, disruption_type, start_date)
|
| 124 |
+
)
|
| 125 |
+
''')
|
| 126 |
+
|
| 127 |
+
# Create indexes
|
| 128 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_orders_customer ON erp_orders (customer_id)')
|
| 129 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_items_order ON erp_order_items (order_id)')
|
| 130 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_items_product ON erp_order_items (product_id)')
|
| 131 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_order_history_order ON erp_order_history (order_id)')
|
| 132 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_invoices_order ON erp_invoices (order_id)')
|
| 133 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_global_disruptions_active ON live_global_disruptions (is_active)')
|
| 134 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_global_disruptions_countries ON live_global_disruptions (source_country, destination_country)')
|
| 135 |
+
|
| 136 |
+
conn.commit()
|
| 137 |
+
conn.close()
|
| 138 |
+
|
| 139 |
+
return True
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"Error initializing SQLite database: {str(e)}")
|
| 142 |
+
return False
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
init_sqlite_db()
|
core/mcp_tools/erp_server.py
CHANGED
|
@@ -1,64 +1,129 @@
|
|
| 1 |
# erp_server.py
|
| 2 |
-
from typing import Dict, Any, List, Optional
|
| 3 |
-
import psycopg2
|
| 4 |
-
from psycopg2.extras import RealDictCursor
|
| 5 |
-
from mcp.server.fastmcp import FastMCP
|
| 6 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
-
import
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
load_dotenv()
|
| 12 |
|
| 13 |
mcp = FastMCP("ERP Database")
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def get_db_connection():
|
| 16 |
-
"""Get database connection
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
@mcp.tool()
|
| 30 |
async def execute_query(query: str, params: Optional[List] = None) -> Dict[str, Any]:
|
| 31 |
"""
|
| 32 |
Execute a custom SQL query on the ERP database.
|
|
|
|
| 33 |
|
| 34 |
Args:
|
| 35 |
-
query (str): SQL query to execute
|
| 36 |
params (List, optional): Parameters for parameterized queries
|
| 37 |
|
| 38 |
Returns:
|
| 39 |
dict: Query results or error message
|
| 40 |
"""
|
| 41 |
try:
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
if params:
|
| 46 |
cursor.execute(query, params)
|
| 47 |
else:
|
| 48 |
cursor.execute(query)
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# Convert RealDictRow to regular dict for JSON serialization
|
| 54 |
results = [dict(row) for row in results]
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
else:
|
| 59 |
-
# For INSERT, UPDATE, DELETE queries
|
| 60 |
-
conn.commit()
|
| 61 |
-
return f"Query executed successfully. {cursor.rowcount} rows affected."
|
| 62 |
|
| 63 |
except Exception as e:
|
| 64 |
return {
|
|
@@ -77,21 +142,33 @@ async def list_erp_tables() -> Dict[str, Any]:
|
|
| 77 |
Returns:
|
| 78 |
dict: List of ERP table names
|
| 79 |
"""
|
| 80 |
-
query = """
|
| 81 |
-
SELECT table_name
|
| 82 |
-
FROM information_schema.tables
|
| 83 |
-
WHERE table_schema = 'public'
|
| 84 |
-
AND table_name LIKE 'erp_%'
|
| 85 |
-
ORDER BY table_name;
|
| 86 |
-
"""
|
| 87 |
-
|
| 88 |
try:
|
| 89 |
-
conn = get_db_connection()
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
table_names = [row['table_name'] for row in results]
|
| 95 |
if not table_names:
|
| 96 |
return {
|
| 97 |
"success": False,
|
|
@@ -125,17 +202,24 @@ async def get_order_status(order_id: int) -> Dict[str, Any]:
|
|
| 125 |
Returns:
|
| 126 |
dict: Order status information including shipping and destination countries
|
| 127 |
"""
|
| 128 |
-
query = """
|
| 129 |
-
SELECT o.*, c.name as customer_name, c.email as customer_email,
|
| 130 |
-
o.shipping_country, o.destination_country
|
| 131 |
-
FROM erp_orders o
|
| 132 |
-
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 133 |
-
WHERE o.order_id = %s;
|
| 134 |
-
"""
|
| 135 |
-
|
| 136 |
try:
|
| 137 |
-
conn = get_db_connection()
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
cursor.execute(query, [order_id])
|
| 140 |
order = cursor.fetchone()
|
| 141 |
|
|
@@ -146,28 +230,28 @@ async def get_order_status(order_id: int) -> Dict[str, Any]:
|
|
| 146 |
}
|
| 147 |
|
| 148 |
# Get order items
|
| 149 |
-
items_query = """
|
| 150 |
SELECT oi.order_item_id, oi.order_id, oi.product_id, oi.quantity, oi.unit_price, oi.subtotal,
|
| 151 |
p.product_name, p.sku
|
| 152 |
FROM erp_order_items oi
|
| 153 |
JOIN erp_products p ON oi.product_id = p.product_id
|
| 154 |
-
WHERE oi.order_id =
|
| 155 |
"""
|
| 156 |
cursor.execute(items_query, [order_id])
|
| 157 |
items = cursor.fetchall()
|
| 158 |
|
| 159 |
# Get order history
|
| 160 |
-
history_query = """
|
| 161 |
SELECT * FROM erp_order_history
|
| 162 |
-
WHERE order_id =
|
| 163 |
ORDER BY timestamp DESC;
|
| 164 |
"""
|
| 165 |
cursor.execute(history_query, [order_id])
|
| 166 |
history = cursor.fetchall()
|
| 167 |
|
| 168 |
-
order_dict = dict(order)
|
| 169 |
-
items_list = [dict(item) for item in items]
|
| 170 |
-
history_list = [dict(entry) for entry in history]
|
| 171 |
|
| 172 |
# Return JSON formatted response
|
| 173 |
return {
|
|
@@ -221,13 +305,27 @@ async def place_new_order(
|
|
| 221 |
dict: New order information
|
| 222 |
"""
|
| 223 |
try:
|
| 224 |
-
conn = get_db_connection()
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
# Calculate total amount based on the total items and their prices
|
| 228 |
total_amount = 0
|
| 229 |
for item in items:
|
| 230 |
-
product_query = "SELECT price FROM erp_products WHERE product_id =
|
| 231 |
cursor.execute(product_query, [item['product_id']])
|
| 232 |
product = cursor.fetchone()
|
| 233 |
if not product:
|
|
@@ -238,21 +336,26 @@ async def place_new_order(
|
|
| 238 |
total_amount += product['price'] * item['quantity']
|
| 239 |
|
| 240 |
# Insert new order into the erp_orders table for the customer
|
| 241 |
-
order_query = """
|
| 242 |
INSERT INTO erp_orders (
|
| 243 |
customer_id, order_date, total_amount, status,
|
| 244 |
shipping_address, shipping_country, destination_country, previous_order_id,
|
| 245 |
estimated_delivery, payment_status
|
| 246 |
) VALUES (
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
)
|
| 251 |
"""
|
|
|
|
| 252 |
cursor.execute(order_query, [
|
| 253 |
customer_id, total_amount, shipping_address, shipping_country, destination_country, previous_order_id
|
| 254 |
])
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
# Insert order items into erp_order_items table for the new order
|
| 258 |
for item in items:
|
|
@@ -262,11 +365,11 @@ async def place_new_order(
|
|
| 262 |
unit_price = product['price']
|
| 263 |
subtotal = unit_price * item['quantity']
|
| 264 |
|
| 265 |
-
item_query = """
|
| 266 |
INSERT INTO erp_order_items (
|
| 267 |
order_id, product_id, quantity, unit_price, subtotal
|
| 268 |
) VALUES (
|
| 269 |
-
|
| 270 |
);
|
| 271 |
"""
|
| 272 |
cursor.execute(item_query, [
|
|
@@ -275,52 +378,66 @@ async def place_new_order(
|
|
| 275 |
])
|
| 276 |
|
| 277 |
# Update product stock
|
| 278 |
-
update_stock_query = """
|
| 279 |
UPDATE erp_products
|
| 280 |
-
SET stock_quantity = stock_quantity -
|
| 281 |
-
WHERE product_id =
|
| 282 |
"""
|
| 283 |
cursor.execute(update_stock_query, [item['quantity'], item['product_id']])
|
| 284 |
|
| 285 |
# Create order history entry into the erp_order_history for the new order
|
| 286 |
-
history_query = """
|
| 287 |
INSERT INTO erp_order_history (
|
| 288 |
order_id, timestamp, status_change, notes, updated_by
|
| 289 |
) VALUES (
|
| 290 |
-
|
| 291 |
);
|
| 292 |
"""
|
| 293 |
cursor.execute(history_query, [new_order_id])
|
| 294 |
|
| 295 |
# If this is a replacement order, add a note to the previous order
|
| 296 |
if previous_order_id:
|
| 297 |
-
prev_order_note_query = """
|
| 298 |
INSERT INTO erp_order_history (
|
| 299 |
order_id, timestamp, status_change, notes, updated_by
|
| 300 |
) VALUES (
|
| 301 |
-
|
| 302 |
);
|
| 303 |
"""
|
| 304 |
cursor.execute(prev_order_note_query, [previous_order_id, new_order_id])
|
| 305 |
|
| 306 |
# Generate invoice for the new order
|
| 307 |
-
invoice_query = """
|
| 308 |
INSERT INTO erp_invoices (
|
| 309 |
order_id, invoice_date, amount, payment_terms, due_date, is_paid, invoice_number
|
| 310 |
) VALUES (
|
| 311 |
-
|
| 312 |
-
)
|
| 313 |
"""
|
| 314 |
-
cursor.execute(invoice_query, [new_order_id, total_amount, new_order_id])
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
conn.commit()
|
| 318 |
|
| 319 |
# Get the complete new order details
|
| 320 |
-
cursor.execute("SELECT * FROM erp_orders WHERE order_id =
|
| 321 |
order = cursor.fetchone()
|
| 322 |
|
| 323 |
-
order_dict =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
# Return JSON formatted response
|
| 326 |
return {
|
|
@@ -330,7 +447,7 @@ async def place_new_order(
|
|
| 330 |
"invoice_id": invoice_id,
|
| 331 |
"total_amount": float(total_amount),
|
| 332 |
"status": order_dict['status'],
|
| 333 |
-
"estimated_delivery":
|
| 334 |
"customer_id": customer_id,
|
| 335 |
"shipping_address": shipping_address,
|
| 336 |
"shipping_country": shipping_country,
|
|
@@ -363,12 +480,18 @@ async def cancel_order(order_id: int, reason: str) -> Dict[str, Any]:
|
|
| 363 |
dict: Result of the cancellation operation
|
| 364 |
"""
|
| 365 |
try:
|
| 366 |
-
conn = get_db_connection()
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
|
| 369 |
# Check if order exists and can be cancelled
|
| 370 |
-
check_query = """
|
| 371 |
-
SELECT status, customer_id FROM erp_orders WHERE order_id =
|
| 372 |
"""
|
| 373 |
cursor.execute(check_query, [order_id])
|
| 374 |
order = cursor.fetchone()
|
|
@@ -386,43 +509,43 @@ async def cancel_order(order_id: int, reason: str) -> Dict[str, Any]:
|
|
| 386 |
}
|
| 387 |
|
| 388 |
# Get order items to restore stock
|
| 389 |
-
items_query = """
|
| 390 |
-
SELECT product_id, quantity FROM erp_order_items WHERE order_id =
|
| 391 |
"""
|
| 392 |
cursor.execute(items_query, [order_id])
|
| 393 |
items = cursor.fetchall()
|
| 394 |
|
| 395 |
# Update order status to Cancelled
|
| 396 |
-
update_query = """
|
| 397 |
UPDATE erp_orders SET status = 'Cancelled', payment_status = 'Cancelled'
|
| 398 |
-
WHERE order_id =
|
| 399 |
"""
|
| 400 |
cursor.execute(update_query, [order_id])
|
| 401 |
|
| 402 |
# Add entry to order history
|
| 403 |
-
history_query = """
|
| 404 |
INSERT INTO erp_order_history (
|
| 405 |
order_id, timestamp, status_change, notes, updated_by
|
| 406 |
) VALUES (
|
| 407 |
-
|
| 408 |
);
|
| 409 |
"""
|
| 410 |
cursor.execute(history_query, [order_id, f"Order cancelled: {reason}"])
|
| 411 |
|
| 412 |
# Restore product stock quantities
|
| 413 |
for item in items:
|
| 414 |
-
restore_stock_query = """
|
| 415 |
UPDATE erp_products
|
| 416 |
-
SET stock_quantity = stock_quantity +
|
| 417 |
-
WHERE product_id =
|
| 418 |
"""
|
| 419 |
cursor.execute(restore_stock_query, [item['quantity'], item['product_id']])
|
| 420 |
|
| 421 |
# Update invoice if exists
|
| 422 |
-
invoice_query = """
|
| 423 |
UPDATE erp_invoices
|
| 424 |
-
SET is_paid =
|
| 425 |
-
WHERE order_id =
|
| 426 |
"""
|
| 427 |
cursor.execute(invoice_query, [order_id])
|
| 428 |
|
|
@@ -465,27 +588,33 @@ async def get_invoice_details(invoice_id: Optional[int] = None, order_id: Option
|
|
| 465 |
}
|
| 466 |
|
| 467 |
try:
|
| 468 |
-
conn = get_db_connection()
|
| 469 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
if invoice_id:
|
| 472 |
-
query = """
|
| 473 |
SELECT i.*, o.order_date, o.status as order_status,
|
| 474 |
c.name as customer_name, c.email as customer_email, c.address as customer_address
|
| 475 |
FROM erp_invoices i
|
| 476 |
JOIN erp_orders o ON i.order_id = o.order_id
|
| 477 |
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 478 |
-
WHERE i.invoice_id =
|
| 479 |
"""
|
| 480 |
cursor.execute(query, [invoice_id])
|
| 481 |
else:
|
| 482 |
-
query = """
|
| 483 |
SELECT i.*, o.order_date, o.status as order_status,
|
| 484 |
c.name as customer_name, c.email as customer_email, c.address as customer_address
|
| 485 |
FROM erp_invoices i
|
| 486 |
JOIN erp_orders o ON i.order_id = o.order_id
|
| 487 |
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 488 |
-
WHERE i.order_id =
|
| 489 |
"""
|
| 490 |
cursor.execute(query, [order_id])
|
| 491 |
|
|
@@ -498,17 +627,36 @@ async def get_invoice_details(invoice_id: Optional[int] = None, order_id: Option
|
|
| 498 |
}
|
| 499 |
|
| 500 |
# Get order items
|
| 501 |
-
items_query = """
|
| 502 |
SELECT oi.*, p.product_name, p.sku
|
| 503 |
FROM erp_order_items oi
|
| 504 |
JOIN erp_products p ON oi.product_id = p.product_id
|
| 505 |
-
WHERE oi.order_id =
|
| 506 |
"""
|
| 507 |
cursor.execute(items_query, [invoice['order_id']])
|
| 508 |
items = cursor.fetchall()
|
| 509 |
|
| 510 |
-
invoice_dict =
|
| 511 |
-
items_list =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
# Return JSON formatted response
|
| 514 |
return {
|
|
@@ -517,10 +665,10 @@ async def get_invoice_details(invoice_id: Optional[int] = None, order_id: Option
|
|
| 517 |
"invoice_id": invoice_dict['invoice_id'],
|
| 518 |
"invoice_number": invoice_dict.get('invoice_number', ''),
|
| 519 |
"order_id": invoice_dict['order_id'],
|
| 520 |
-
"order_date":
|
| 521 |
"order_status": invoice_dict['order_status'],
|
| 522 |
"amount": float(invoice_dict['amount']),
|
| 523 |
-
"due_date":
|
| 524 |
"payment_status": "Paid" if invoice_dict['is_paid'] else "Unpaid",
|
| 525 |
"customer": {
|
| 526 |
"name": invoice_dict['customer_name'],
|
|
|
|
| 1 |
# erp_server.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
+
import sys
|
| 4 |
+
from typing import TypedDict, List, Union
|
| 5 |
+
|
| 6 |
+
# Add the project root directory to the Python path
|
| 7 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
| 8 |
+
|
| 9 |
+
from typing import Dict, Any, List, Optional
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
+
from datetime import datetime, date
|
| 12 |
+
import pathlib, logging
|
| 13 |
+
|
| 14 |
+
# Initialize logging
|
| 15 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 16 |
+
|
| 17 |
+
# Import both database libraries
|
| 18 |
+
import sqlite3
|
| 19 |
+
try:
|
| 20 |
+
import psycopg2
|
| 21 |
+
from psycopg2.extras import RealDictCursor
|
| 22 |
+
POSTGRES_AVAILABLE = True
|
| 23 |
+
except ImportError:
|
| 24 |
+
POSTGRES_AVAILABLE = False
|
| 25 |
+
|
| 26 |
+
# Import SQLite initialization function
|
| 27 |
+
from core.mcp_tools.erp_db_init import init_sqlite_db
|
| 28 |
+
|
| 29 |
+
from mcp.server.fastmcp import FastMCP
|
| 30 |
|
| 31 |
load_dotenv()
|
| 32 |
|
| 33 |
mcp = FastMCP("ERP Database")
|
| 34 |
|
| 35 |
+
# Helper function to convert SQLite row to dict
|
| 36 |
+
def dict_factory(cursor, row):
|
| 37 |
+
d = {}
|
| 38 |
+
for idx, col in enumerate(cursor.description):
|
| 39 |
+
d[col[0]] = row[idx]
|
| 40 |
+
return d
|
| 41 |
+
|
| 42 |
+
# Helper function to handle date serialization for JSON
|
| 43 |
+
def serialize_dates(obj):
|
| 44 |
+
if isinstance(obj, (date, datetime)):
|
| 45 |
+
return obj.isoformat()
|
| 46 |
+
return obj
|
| 47 |
+
|
| 48 |
def get_db_connection():
|
| 49 |
+
"""Get database connection based on configuration."""
|
| 50 |
+
# Default to SQLite unless explicitly set to use PostgreSQL
|
| 51 |
+
db_type = os.getenv("ERP_DB_TYPE", "sqlite").lower()
|
| 52 |
+
|
| 53 |
+
logging.info(f"Connecting to {db_type} database...")
|
| 54 |
+
|
| 55 |
+
if db_type == "postgres" and POSTGRES_AVAILABLE:
|
| 56 |
+
try:
|
| 57 |
+
conn = psycopg2.connect(
|
| 58 |
+
host=os.getenv("POSTGRES_HOST", "localhost"),
|
| 59 |
+
database=os.getenv("POSTGRES_DB", "erp_db"),
|
| 60 |
+
user=os.getenv("POSTGRES_USER", "postgres"),
|
| 61 |
+
password=os.getenv("POSTGRES_PASSWORD", ""),
|
| 62 |
+
port=os.getenv("POSTGRES_PORT", "5432")
|
| 63 |
+
)
|
| 64 |
+
return conn, "postgres"
|
| 65 |
+
except Exception as e:
|
| 66 |
+
raise Exception(f"PostgreSQL connection failed: {str(e)}")
|
| 67 |
+
else:
|
| 68 |
+
try:
|
| 69 |
+
# Get SQLite database path from environment or use default
|
| 70 |
+
db_path = os.getenv("SQLITE_DB_PATH", "./data/erp_db.sqlite")
|
| 71 |
+
|
| 72 |
+
# Ensure database is initialized
|
| 73 |
+
db_dir = pathlib.Path(os.path.dirname(db_path))
|
| 74 |
+
if not db_dir.exists() or not pathlib.Path(db_path).exists():
|
| 75 |
+
init_sqlite_db(db_path)
|
| 76 |
+
|
| 77 |
+
conn = sqlite3.connect(db_path)
|
| 78 |
+
conn.row_factory = dict_factory
|
| 79 |
+
|
| 80 |
+
# Enable foreign keys
|
| 81 |
+
conn.execute("PRAGMA foreign_keys = ON")
|
| 82 |
+
|
| 83 |
+
return conn, "sqlite"
|
| 84 |
+
except Exception as e:
|
| 85 |
+
raise Exception(f"SQLite connection failed: {str(e)}")
|
| 86 |
|
| 87 |
@mcp.tool()
|
| 88 |
async def execute_query(query: str, params: Optional[List] = None) -> Dict[str, Any]:
|
| 89 |
"""
|
| 90 |
Execute a custom SQL query on the ERP database.
|
| 91 |
+
Only SELECT statements are allowed for security reasons.
|
| 92 |
|
| 93 |
Args:
|
| 94 |
+
query (str): SQL query to execute (must be a SELECT statement)
|
| 95 |
params (List, optional): Parameters for parameterized queries
|
| 96 |
|
| 97 |
Returns:
|
| 98 |
dict: Query results or error message
|
| 99 |
"""
|
| 100 |
try:
|
| 101 |
+
# Check if it's a SELECT query
|
| 102 |
+
if not query.strip().upper().startswith('SELECT'):
|
| 103 |
+
return {
|
| 104 |
+
"success": False,
|
| 105 |
+
"error": "Only SELECT statements are allowed for security reasons."
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
conn, db_type = get_db_connection()
|
| 109 |
+
|
| 110 |
+
if db_type == "postgres":
|
| 111 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 112 |
+
else: # sqlite
|
| 113 |
+
cursor = conn.cursor()
|
| 114 |
|
| 115 |
if params:
|
| 116 |
cursor.execute(query, params)
|
| 117 |
else:
|
| 118 |
cursor.execute(query)
|
| 119 |
|
| 120 |
+
results = cursor.fetchall()
|
| 121 |
+
# Convert results to regular dict for JSON serialization
|
| 122 |
+
if db_type == "postgres":
|
|
|
|
| 123 |
results = [dict(row) for row in results]
|
| 124 |
+
if not results:
|
| 125 |
+
return "No results found"
|
| 126 |
+
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
return {
|
|
|
|
| 142 |
Returns:
|
| 143 |
dict: List of ERP table names
|
| 144 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
try:
|
| 146 |
+
conn, db_type = get_db_connection()
|
| 147 |
+
|
| 148 |
+
if db_type == "postgres":
|
| 149 |
+
query = """
|
| 150 |
+
SELECT table_name
|
| 151 |
+
FROM information_schema.tables
|
| 152 |
+
WHERE table_schema = 'public'
|
| 153 |
+
AND table_name LIKE 'erp_%'
|
| 154 |
+
ORDER BY table_name;
|
| 155 |
+
"""
|
| 156 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 157 |
+
cursor.execute(query)
|
| 158 |
+
results = cursor.fetchall()
|
| 159 |
+
table_names = [row['table_name'] for row in results]
|
| 160 |
+
else: # sqlite
|
| 161 |
+
query = """
|
| 162 |
+
SELECT name as table_name
|
| 163 |
+
FROM sqlite_master
|
| 164 |
+
WHERE type='table' AND name LIKE 'erp_%'
|
| 165 |
+
ORDER BY name;
|
| 166 |
+
"""
|
| 167 |
+
cursor = conn.cursor()
|
| 168 |
+
cursor.execute(query)
|
| 169 |
+
results = cursor.fetchall()
|
| 170 |
+
table_names = [row['table_name'] for row in results]
|
| 171 |
|
|
|
|
| 172 |
if not table_names:
|
| 173 |
return {
|
| 174 |
"success": False,
|
|
|
|
| 202 |
Returns:
|
| 203 |
dict: Order status information including shipping and destination countries
|
| 204 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
try:
|
| 206 |
+
conn, db_type = get_db_connection()
|
| 207 |
+
|
| 208 |
+
if db_type == "postgres":
|
| 209 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 210 |
+
param_placeholder = "%s"
|
| 211 |
+
else: # sqlite
|
| 212 |
+
cursor = conn.cursor()
|
| 213 |
+
param_placeholder = "?"
|
| 214 |
+
|
| 215 |
+
query = f"""
|
| 216 |
+
SELECT o.*, c.name as customer_name, c.email as customer_email,
|
| 217 |
+
o.shipping_country, o.destination_country
|
| 218 |
+
FROM erp_orders o
|
| 219 |
+
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 220 |
+
WHERE o.order_id = {param_placeholder};
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
cursor.execute(query, [order_id])
|
| 224 |
order = cursor.fetchone()
|
| 225 |
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
# Get order items
|
| 233 |
+
items_query = f"""
|
| 234 |
SELECT oi.order_item_id, oi.order_id, oi.product_id, oi.quantity, oi.unit_price, oi.subtotal,
|
| 235 |
p.product_name, p.sku
|
| 236 |
FROM erp_order_items oi
|
| 237 |
JOIN erp_products p ON oi.product_id = p.product_id
|
| 238 |
+
WHERE oi.order_id = {param_placeholder};
|
| 239 |
"""
|
| 240 |
cursor.execute(items_query, [order_id])
|
| 241 |
items = cursor.fetchall()
|
| 242 |
|
| 243 |
# Get order history
|
| 244 |
+
history_query = f"""
|
| 245 |
SELECT * FROM erp_order_history
|
| 246 |
+
WHERE order_id = {param_placeholder}
|
| 247 |
ORDER BY timestamp DESC;
|
| 248 |
"""
|
| 249 |
cursor.execute(history_query, [order_id])
|
| 250 |
history = cursor.fetchall()
|
| 251 |
|
| 252 |
+
order_dict = order if db_type == "sqlite" else dict(order)
|
| 253 |
+
items_list = items if db_type == "sqlite" else [dict(item) for item in items]
|
| 254 |
+
history_list = history if db_type == "sqlite" else [dict(entry) for entry in history]
|
| 255 |
|
| 256 |
# Return JSON formatted response
|
| 257 |
return {
|
|
|
|
| 305 |
dict: New order information
|
| 306 |
"""
|
| 307 |
try:
|
| 308 |
+
conn, db_type = get_db_connection()
|
| 309 |
+
|
| 310 |
+
if db_type == "postgres":
|
| 311 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 312 |
+
param_placeholder = "%s"
|
| 313 |
+
returning_clause = "RETURNING order_id"
|
| 314 |
+
date_interval = "CURRENT_DATE + INTERVAL '7 days'"
|
| 315 |
+
due_date_interval = "CURRENT_DATE + INTERVAL '30 days'"
|
| 316 |
+
concat_op = "||"
|
| 317 |
+
else: # sqlite
|
| 318 |
+
cursor = conn.cursor()
|
| 319 |
+
param_placeholder = "?"
|
| 320 |
+
returning_clause = ""
|
| 321 |
+
date_interval = "date('now', '+7 days')"
|
| 322 |
+
due_date_interval = "date('now', '+30 days')"
|
| 323 |
+
concat_op = "||"
|
| 324 |
|
| 325 |
# Calculate total amount based on the total items and their prices
|
| 326 |
total_amount = 0
|
| 327 |
for item in items:
|
| 328 |
+
product_query = f"SELECT price FROM erp_products WHERE product_id = {param_placeholder};"
|
| 329 |
cursor.execute(product_query, [item['product_id']])
|
| 330 |
product = cursor.fetchone()
|
| 331 |
if not product:
|
|
|
|
| 336 |
total_amount += product['price'] * item['quantity']
|
| 337 |
|
| 338 |
# Insert new order into the erp_orders table for the customer
|
| 339 |
+
order_query = f"""
|
| 340 |
INSERT INTO erp_orders (
|
| 341 |
customer_id, order_date, total_amount, status,
|
| 342 |
shipping_address, shipping_country, destination_country, previous_order_id,
|
| 343 |
estimated_delivery, payment_status
|
| 344 |
) VALUES (
|
| 345 |
+
{param_placeholder}, CURRENT_DATE, {param_placeholder}, 'Processing',
|
| 346 |
+
{param_placeholder}, {param_placeholder}, {param_placeholder}, {param_placeholder},
|
| 347 |
+
{date_interval}, 'Pending'
|
| 348 |
+
) {returning_clause};
|
| 349 |
"""
|
| 350 |
+
|
| 351 |
cursor.execute(order_query, [
|
| 352 |
customer_id, total_amount, shipping_address, shipping_country, destination_country, previous_order_id
|
| 353 |
])
|
| 354 |
+
|
| 355 |
+
if db_type == "postgres":
|
| 356 |
+
new_order_id = cursor.fetchone()['order_id']
|
| 357 |
+
else: # sqlite
|
| 358 |
+
new_order_id = cursor.lastrowid
|
| 359 |
|
| 360 |
# Insert order items into erp_order_items table for the new order
|
| 361 |
for item in items:
|
|
|
|
| 365 |
unit_price = product['price']
|
| 366 |
subtotal = unit_price * item['quantity']
|
| 367 |
|
| 368 |
+
item_query = f"""
|
| 369 |
INSERT INTO erp_order_items (
|
| 370 |
order_id, product_id, quantity, unit_price, subtotal
|
| 371 |
) VALUES (
|
| 372 |
+
{param_placeholder}, {param_placeholder}, {param_placeholder}, {param_placeholder}, {param_placeholder}
|
| 373 |
);
|
| 374 |
"""
|
| 375 |
cursor.execute(item_query, [
|
|
|
|
| 378 |
])
|
| 379 |
|
| 380 |
# Update product stock
|
| 381 |
+
update_stock_query = f"""
|
| 382 |
UPDATE erp_products
|
| 383 |
+
SET stock_quantity = stock_quantity - {param_placeholder}
|
| 384 |
+
WHERE product_id = {param_placeholder};
|
| 385 |
"""
|
| 386 |
cursor.execute(update_stock_query, [item['quantity'], item['product_id']])
|
| 387 |
|
| 388 |
# Create order history entry into the erp_order_history for the new order
|
| 389 |
+
history_query = f"""
|
| 390 |
INSERT INTO erp_order_history (
|
| 391 |
order_id, timestamp, status_change, notes, updated_by
|
| 392 |
) VALUES (
|
| 393 |
+
{param_placeholder}, CURRENT_TIMESTAMP, 'Order Created', 'New order placed', 'System'
|
| 394 |
);
|
| 395 |
"""
|
| 396 |
cursor.execute(history_query, [new_order_id])
|
| 397 |
|
| 398 |
# If this is a replacement order, add a note to the previous order
|
| 399 |
if previous_order_id:
|
| 400 |
+
prev_order_note_query = f"""
|
| 401 |
INSERT INTO erp_order_history (
|
| 402 |
order_id, timestamp, status_change, notes, updated_by
|
| 403 |
) VALUES (
|
| 404 |
+
{param_placeholder}, CURRENT_TIMESTAMP, 'Replaced', 'Order replaced by order #' {concat_op} {param_placeholder}, 'System'
|
| 405 |
);
|
| 406 |
"""
|
| 407 |
cursor.execute(prev_order_note_query, [previous_order_id, new_order_id])
|
| 408 |
|
| 409 |
# Generate invoice for the new order
|
| 410 |
+
invoice_query = f"""
|
| 411 |
INSERT INTO erp_invoices (
|
| 412 |
order_id, invoice_date, amount, payment_terms, due_date, is_paid, invoice_number
|
| 413 |
) VALUES (
|
| 414 |
+
{param_placeholder}, CURRENT_DATE, {param_placeholder}, 'Net 30', {due_date_interval}, 0, 'INV-' {concat_op} {param_placeholder}
|
| 415 |
+
) {returning_clause};
|
| 416 |
"""
|
| 417 |
+
cursor.execute(invoice_query, [new_order_id, total_amount, str(new_order_id)])
|
| 418 |
+
|
| 419 |
+
if db_type == "postgres":
|
| 420 |
+
invoice_id = cursor.fetchone()['invoice_id']
|
| 421 |
+
else: # sqlite
|
| 422 |
+
invoice_id = cursor.lastrowid
|
| 423 |
|
| 424 |
conn.commit()
|
| 425 |
|
| 426 |
# Get the complete new order details
|
| 427 |
+
cursor.execute(f"SELECT * FROM erp_orders WHERE order_id = {param_placeholder};", [new_order_id])
|
| 428 |
order = cursor.fetchone()
|
| 429 |
|
| 430 |
+
order_dict = order
|
| 431 |
+
|
| 432 |
+
# Format date for response
|
| 433 |
+
estimated_delivery = order_dict.get('estimated_delivery')
|
| 434 |
+
if estimated_delivery:
|
| 435 |
+
if isinstance(estimated_delivery, str):
|
| 436 |
+
estimated_delivery_str = estimated_delivery
|
| 437 |
+
else:
|
| 438 |
+
estimated_delivery_str = estimated_delivery.strftime("%Y-%m-%d") if hasattr(estimated_delivery, 'strftime') else str(estimated_delivery)
|
| 439 |
+
else:
|
| 440 |
+
estimated_delivery_str = None
|
| 441 |
|
| 442 |
# Return JSON formatted response
|
| 443 |
return {
|
|
|
|
| 447 |
"invoice_id": invoice_id,
|
| 448 |
"total_amount": float(total_amount),
|
| 449 |
"status": order_dict['status'],
|
| 450 |
+
"estimated_delivery": estimated_delivery_str,
|
| 451 |
"customer_id": customer_id,
|
| 452 |
"shipping_address": shipping_address,
|
| 453 |
"shipping_country": shipping_country,
|
|
|
|
| 480 |
dict: Result of the cancellation operation
|
| 481 |
"""
|
| 482 |
try:
|
| 483 |
+
conn, db_type = get_db_connection()
|
| 484 |
+
|
| 485 |
+
if db_type == "postgres":
|
| 486 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 487 |
+
param_placeholder = "%s"
|
| 488 |
+
else: # sqlite
|
| 489 |
+
cursor = conn.cursor()
|
| 490 |
+
param_placeholder = "?"
|
| 491 |
|
| 492 |
# Check if order exists and can be cancelled
|
| 493 |
+
check_query = f"""
|
| 494 |
+
SELECT status, customer_id FROM erp_orders WHERE order_id = {param_placeholder};
|
| 495 |
"""
|
| 496 |
cursor.execute(check_query, [order_id])
|
| 497 |
order = cursor.fetchone()
|
|
|
|
| 509 |
}
|
| 510 |
|
| 511 |
# Get order items to restore stock
|
| 512 |
+
items_query = f"""
|
| 513 |
+
SELECT product_id, quantity FROM erp_order_items WHERE order_id = {param_placeholder};
|
| 514 |
"""
|
| 515 |
cursor.execute(items_query, [order_id])
|
| 516 |
items = cursor.fetchall()
|
| 517 |
|
| 518 |
# Update order status to Cancelled
|
| 519 |
+
update_query = f"""
|
| 520 |
UPDATE erp_orders SET status = 'Cancelled', payment_status = 'Cancelled'
|
| 521 |
+
WHERE order_id = {param_placeholder};
|
| 522 |
"""
|
| 523 |
cursor.execute(update_query, [order_id])
|
| 524 |
|
| 525 |
# Add entry to order history
|
| 526 |
+
history_query = f"""
|
| 527 |
INSERT INTO erp_order_history (
|
| 528 |
order_id, timestamp, status_change, notes, updated_by
|
| 529 |
) VALUES (
|
| 530 |
+
{param_placeholder}, CURRENT_TIMESTAMP, 'Cancelled', {param_placeholder}, 'System'
|
| 531 |
);
|
| 532 |
"""
|
| 533 |
cursor.execute(history_query, [order_id, f"Order cancelled: {reason}"])
|
| 534 |
|
| 535 |
# Restore product stock quantities
|
| 536 |
for item in items:
|
| 537 |
+
restore_stock_query = f"""
|
| 538 |
UPDATE erp_products
|
| 539 |
+
SET stock_quantity = stock_quantity + {param_placeholder}
|
| 540 |
+
WHERE product_id = {param_placeholder};
|
| 541 |
"""
|
| 542 |
cursor.execute(restore_stock_query, [item['quantity'], item['product_id']])
|
| 543 |
|
| 544 |
# Update invoice if exists
|
| 545 |
+
invoice_query = f"""
|
| 546 |
UPDATE erp_invoices
|
| 547 |
+
SET is_paid = 0
|
| 548 |
+
WHERE order_id = {param_placeholder};
|
| 549 |
"""
|
| 550 |
cursor.execute(invoice_query, [order_id])
|
| 551 |
|
|
|
|
| 588 |
}
|
| 589 |
|
| 590 |
try:
|
| 591 |
+
conn, db_type = get_db_connection()
|
| 592 |
+
|
| 593 |
+
if db_type == "postgres":
|
| 594 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 595 |
+
param_placeholder = "%s"
|
| 596 |
+
else: # sqlite
|
| 597 |
+
cursor = conn.cursor()
|
| 598 |
+
param_placeholder = "?"
|
| 599 |
|
| 600 |
if invoice_id:
|
| 601 |
+
query = f"""
|
| 602 |
SELECT i.*, o.order_date, o.status as order_status,
|
| 603 |
c.name as customer_name, c.email as customer_email, c.address as customer_address
|
| 604 |
FROM erp_invoices i
|
| 605 |
JOIN erp_orders o ON i.order_id = o.order_id
|
| 606 |
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 607 |
+
WHERE i.invoice_id = {param_placeholder};
|
| 608 |
"""
|
| 609 |
cursor.execute(query, [invoice_id])
|
| 610 |
else:
|
| 611 |
+
query = f"""
|
| 612 |
SELECT i.*, o.order_date, o.status as order_status,
|
| 613 |
c.name as customer_name, c.email as customer_email, c.address as customer_address
|
| 614 |
FROM erp_invoices i
|
| 615 |
JOIN erp_orders o ON i.order_id = o.order_id
|
| 616 |
JOIN erp_customers c ON o.customer_id = c.customer_id
|
| 617 |
+
WHERE i.order_id = {param_placeholder};
|
| 618 |
"""
|
| 619 |
cursor.execute(query, [order_id])
|
| 620 |
|
|
|
|
| 627 |
}
|
| 628 |
|
| 629 |
# Get order items
|
| 630 |
+
items_query = f"""
|
| 631 |
SELECT oi.*, p.product_name, p.sku
|
| 632 |
FROM erp_order_items oi
|
| 633 |
JOIN erp_products p ON oi.product_id = p.product_id
|
| 634 |
+
WHERE oi.order_id = {param_placeholder};
|
| 635 |
"""
|
| 636 |
cursor.execute(items_query, [invoice['order_id']])
|
| 637 |
items = cursor.fetchall()
|
| 638 |
|
| 639 |
+
invoice_dict = invoice
|
| 640 |
+
items_list = items
|
| 641 |
+
|
| 642 |
+
# Format dates for response
|
| 643 |
+
order_date = invoice_dict.get('order_date')
|
| 644 |
+
if order_date:
|
| 645 |
+
if isinstance(order_date, str):
|
| 646 |
+
order_date_str = order_date
|
| 647 |
+
else:
|
| 648 |
+
order_date_str = order_date.strftime("%Y-%m-%d") if hasattr(order_date, 'strftime') else str(order_date)
|
| 649 |
+
else:
|
| 650 |
+
order_date_str = None
|
| 651 |
+
|
| 652 |
+
due_date = invoice_dict.get('due_date')
|
| 653 |
+
if due_date:
|
| 654 |
+
if isinstance(due_date, str):
|
| 655 |
+
due_date_str = due_date
|
| 656 |
+
else:
|
| 657 |
+
due_date_str = due_date.strftime("%Y-%m-%d") if hasattr(due_date, 'strftime') else str(due_date)
|
| 658 |
+
else:
|
| 659 |
+
due_date_str = None
|
| 660 |
|
| 661 |
# Return JSON formatted response
|
| 662 |
return {
|
|
|
|
| 665 |
"invoice_id": invoice_dict['invoice_id'],
|
| 666 |
"invoice_number": invoice_dict.get('invoice_number', ''),
|
| 667 |
"order_id": invoice_dict['order_id'],
|
| 668 |
+
"order_date": order_date_str,
|
| 669 |
"order_status": invoice_dict['order_status'],
|
| 670 |
"amount": float(invoice_dict['amount']),
|
| 671 |
+
"due_date": due_date_str,
|
| 672 |
"payment_status": "Paid" if invoice_dict['is_paid'] else "Unpaid",
|
| 673 |
"customer": {
|
| 674 |
"name": invoice_dict['customer_name'],
|
core/mcp_tools/global_disruptions_server.py
CHANGED
|
@@ -1,29 +1,133 @@
|
|
| 1 |
# global_disruptions_server.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from typing import Dict, Any, List, Optional
|
| 3 |
-
import psycopg2
|
| 4 |
-
from psycopg2.extras import RealDictCursor
|
| 5 |
-
from mcp.server.fastmcp import FastMCP
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
from datetime import datetime, date
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
load_dotenv()
|
| 11 |
|
| 12 |
mcp = FastMCP("Global Disruptions Database")
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
def get_db_connection():
|
| 15 |
-
"""Get database connection
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
try:
|
| 17 |
-
conn =
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
@mcp.tool()
|
| 29 |
async def get_active_disruptions(source_country: Optional[str] = None, destination_country: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -38,11 +142,19 @@ async def get_active_disruptions(source_country: Optional[str] = None, destinati
|
|
| 38 |
dict: Information about current disruptions that might affect shipping
|
| 39 |
"""
|
| 40 |
try:
|
| 41 |
-
conn = get_db_connection()
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# Base query to get disruptions
|
| 45 |
-
query = """
|
| 46 |
SELECT
|
| 47 |
disruption_id,
|
| 48 |
source_country,
|
|
@@ -60,29 +172,29 @@ async def get_active_disruptions(source_country: Optional[str] = None, destinati
|
|
| 60 |
FROM
|
| 61 |
live_global_disruptions
|
| 62 |
WHERE
|
| 63 |
-
is_active =
|
| 64 |
"""
|
| 65 |
|
| 66 |
params = []
|
| 67 |
|
| 68 |
# Add filters for specific countries if provided
|
| 69 |
if source_country and destination_country:
|
| 70 |
-
query += """
|
| 71 |
AND (
|
| 72 |
-
(source_country =
|
| 73 |
-
(source_country =
|
| 74 |
-
(destination_country =
|
| 75 |
)
|
| 76 |
"""
|
| 77 |
params.extend([source_country, destination_country, source_country, destination_country])
|
| 78 |
elif source_country:
|
| 79 |
-
query += """
|
| 80 |
-
AND (source_country =
|
| 81 |
"""
|
| 82 |
params.append(source_country)
|
| 83 |
elif destination_country:
|
| 84 |
-
query += """
|
| 85 |
-
AND (destination_country =
|
| 86 |
"""
|
| 87 |
params.append(destination_country)
|
| 88 |
|
|
@@ -92,8 +204,6 @@ async def get_active_disruptions(source_country: Optional[str] = None, destinati
|
|
| 92 |
severity DESC,
|
| 93 |
updated_at DESC;
|
| 94 |
"""
|
| 95 |
-
|
| 96 |
-
print(query, params) # Debugging line to see the query and parameters
|
| 97 |
|
| 98 |
cursor.execute(query, params)
|
| 99 |
disruptions = cursor.fetchall()
|
|
@@ -126,7 +236,7 @@ async def get_active_disruptions(source_country: Optional[str] = None, destinati
|
|
| 126 |
# Format the response
|
| 127 |
formatted_disruptions = []
|
| 128 |
for disruption in disruptions:
|
| 129 |
-
d = dict(disruption)
|
| 130 |
|
| 131 |
# Format severity level as text
|
| 132 |
severity = d['severity']
|
|
|
|
| 1 |
# global_disruptions_server.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from typing import TypedDict, List, Union
|
| 6 |
+
|
| 7 |
+
# Add the project root directory to the Python path
|
| 8 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
| 9 |
+
|
| 10 |
from typing import Dict, Any, List, Optional
|
|
|
|
|
|
|
|
|
|
| 11 |
import os
|
| 12 |
+
import sys
|
| 13 |
+
import pathlib
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
from datetime import datetime, date
|
| 16 |
|
| 17 |
+
# Import both database libraries
|
| 18 |
+
import sqlite3
|
| 19 |
+
try:
|
| 20 |
+
import psycopg2
|
| 21 |
+
from psycopg2.extras import RealDictCursor
|
| 22 |
+
POSTGRES_AVAILABLE = True
|
| 23 |
+
except ImportError:
|
| 24 |
+
POSTGRES_AVAILABLE = False
|
| 25 |
+
|
| 26 |
+
# Import SQLite initialization function
|
| 27 |
+
from core.mcp_tools.erp_db_init import init_sqlite_db
|
| 28 |
+
from mcp.server.fastmcp import FastMCP
|
| 29 |
+
|
| 30 |
load_dotenv()
|
| 31 |
|
| 32 |
mcp = FastMCP("Global Disruptions Database")
|
| 33 |
|
| 34 |
+
# Helper function to convert SQLite row to dict
|
| 35 |
+
def dict_factory(cursor, row):
|
| 36 |
+
d = {}
|
| 37 |
+
for idx, col in enumerate(cursor.description):
|
| 38 |
+
d[col[0]] = row[idx]
|
| 39 |
+
return d
|
| 40 |
+
|
| 41 |
+
# Helper function to handle date serialization for JSON
|
| 42 |
+
def serialize_dates(obj):
|
| 43 |
+
if isinstance(obj, (date, datetime)):
|
| 44 |
+
return obj.isoformat()
|
| 45 |
+
return obj
|
| 46 |
+
|
| 47 |
def get_db_connection():
|
| 48 |
+
"""Get database connection based on configuration."""
|
| 49 |
+
# Default to SQLite unless explicitly set to use PostgreSQL
|
| 50 |
+
db_type = os.getenv("DISRUPTIONS_DB_TYPE", "sqlite").lower()
|
| 51 |
+
|
| 52 |
+
if db_type == "postgres" and POSTGRES_AVAILABLE:
|
| 53 |
+
try:
|
| 54 |
+
conn = psycopg2.connect(
|
| 55 |
+
host=os.getenv("POSTGRES_HOST", "localhost"),
|
| 56 |
+
database=os.getenv("POSTGRES_DB", "erp_db"),
|
| 57 |
+
user=os.getenv("POSTGRES_USER", "postgres"),
|
| 58 |
+
password=os.getenv("POSTGRES_PASSWORD", ""),
|
| 59 |
+
port=os.getenv("POSTGRES_PORT", "5432")
|
| 60 |
+
)
|
| 61 |
+
return conn, "postgres"
|
| 62 |
+
except Exception as e:
|
| 63 |
+
raise Exception(f"PostgreSQL connection failed: {str(e)}")
|
| 64 |
+
else:
|
| 65 |
+
try:
|
| 66 |
+
# Get SQLite database path from environment or use default
|
| 67 |
+
db_path = os.getenv("SQLITE_DB_PATH", "./data/erp_db.sqlite")
|
| 68 |
+
|
| 69 |
+
# Ensure database is initialized
|
| 70 |
+
db_dir = pathlib.Path(os.path.dirname(db_path))
|
| 71 |
+
if not db_dir.exists() or not pathlib.Path(db_path).exists():
|
| 72 |
+
init_sqlite_db(db_path)
|
| 73 |
+
|
| 74 |
+
conn = sqlite3.connect(db_path)
|
| 75 |
+
conn.row_factory = dict_factory
|
| 76 |
+
|
| 77 |
+
# Enable foreign keys
|
| 78 |
+
conn.execute("PRAGMA foreign_keys = ON")
|
| 79 |
+
|
| 80 |
+
return conn, "sqlite"
|
| 81 |
+
except Exception as e:
|
| 82 |
+
raise Exception(f"SQLite connection failed: {str(e)}")
|
| 83 |
+
|
| 84 |
+
@mcp.tool()
|
| 85 |
+
async def execute_query(query: str, params: Optional[List] = None) -> Dict[str, Any]:
|
| 86 |
+
"""
|
| 87 |
+
Execute a custom SQL query on the Global Disruptions database.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
query (str): SQL query to execute
|
| 91 |
+
params (List, optional): Parameters for parameterized queries
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
dict: Query results or error message
|
| 95 |
+
"""
|
| 96 |
try:
|
| 97 |
+
conn, db_type = get_db_connection()
|
| 98 |
+
|
| 99 |
+
if db_type == "postgres":
|
| 100 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 101 |
+
else: # sqlite
|
| 102 |
+
cursor = conn.cursor()
|
| 103 |
+
|
| 104 |
+
if params:
|
| 105 |
+
cursor.execute(query, params)
|
| 106 |
+
else:
|
| 107 |
+
cursor.execute(query)
|
| 108 |
+
|
| 109 |
+
# Check if it's a SELECT query
|
| 110 |
+
if query.strip().upper().startswith('SELECT'):
|
| 111 |
+
results = cursor.fetchall()
|
| 112 |
+
# Convert results to regular dict for JSON serialization
|
| 113 |
+
if db_type == "postgres":
|
| 114 |
+
results = [dict(row) for row in results]
|
| 115 |
+
if not results:
|
| 116 |
+
return "No results found"
|
| 117 |
+
return results
|
| 118 |
+
else:
|
| 119 |
+
# For INSERT, UPDATE, DELETE queries
|
| 120 |
+
conn.commit()
|
| 121 |
+
return f"Query executed successfully. {cursor.rowcount} rows affected."
|
| 122 |
+
|
| 123 |
except Exception as e:
|
| 124 |
+
return {
|
| 125 |
+
"success": False,
|
| 126 |
+
"error": str(e)
|
| 127 |
+
}
|
| 128 |
+
finally:
|
| 129 |
+
if 'conn' in locals():
|
| 130 |
+
conn.close()
|
| 131 |
|
| 132 |
@mcp.tool()
|
| 133 |
async def get_active_disruptions(source_country: Optional[str] = None, destination_country: Optional[str] = None) -> Dict[str, Any]:
|
|
|
|
| 142 |
dict: Information about current disruptions that might affect shipping
|
| 143 |
"""
|
| 144 |
try:
|
| 145 |
+
conn, db_type = get_db_connection()
|
| 146 |
+
|
| 147 |
+
if db_type == "postgres":
|
| 148 |
+
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
| 149 |
+
param_placeholder = "%s"
|
| 150 |
+
true_value = "TRUE"
|
| 151 |
+
else: # sqlite
|
| 152 |
+
cursor = conn.cursor()
|
| 153 |
+
param_placeholder = "?"
|
| 154 |
+
true_value = "1"
|
| 155 |
|
| 156 |
# Base query to get disruptions
|
| 157 |
+
query = f"""
|
| 158 |
SELECT
|
| 159 |
disruption_id,
|
| 160 |
source_country,
|
|
|
|
| 172 |
FROM
|
| 173 |
live_global_disruptions
|
| 174 |
WHERE
|
| 175 |
+
is_active = {true_value}
|
| 176 |
"""
|
| 177 |
|
| 178 |
params = []
|
| 179 |
|
| 180 |
# Add filters for specific countries if provided
|
| 181 |
if source_country and destination_country:
|
| 182 |
+
query += f"""
|
| 183 |
AND (
|
| 184 |
+
(source_country = {param_placeholder} AND destination_country = {param_placeholder}) OR
|
| 185 |
+
(source_country = {param_placeholder}) OR
|
| 186 |
+
(destination_country = {param_placeholder})
|
| 187 |
)
|
| 188 |
"""
|
| 189 |
params.extend([source_country, destination_country, source_country, destination_country])
|
| 190 |
elif source_country:
|
| 191 |
+
query += f"""
|
| 192 |
+
AND (source_country = {param_placeholder})
|
| 193 |
"""
|
| 194 |
params.append(source_country)
|
| 195 |
elif destination_country:
|
| 196 |
+
query += f"""
|
| 197 |
+
AND (destination_country = {param_placeholder})
|
| 198 |
"""
|
| 199 |
params.append(destination_country)
|
| 200 |
|
|
|
|
| 204 |
severity DESC,
|
| 205 |
updated_at DESC;
|
| 206 |
"""
|
|
|
|
|
|
|
| 207 |
|
| 208 |
cursor.execute(query, params)
|
| 209 |
disruptions = cursor.fetchall()
|
|
|
|
| 236 |
# Format the response
|
| 237 |
formatted_disruptions = []
|
| 238 |
for disruption in disruptions:
|
| 239 |
+
d = disruption if db_type == "sqlite" else dict(disruption)
|
| 240 |
|
| 241 |
# Format severity level as text
|
| 242 |
severity = d['severity']
|
core/mcp_tools/populate_erp_db.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# populate_erp_db.py
|
| 2 |
+
import sqlite3
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
import datetime
|
| 6 |
+
import random
|
| 7 |
+
from erp_db_init import init_sqlite_db
|
| 8 |
+
|
| 9 |
+
def populate_erp_db(db_path='./data/erp_db.sqlite'):
|
| 10 |
+
"""Populate SQLite database with sample ERP data."""
|
| 11 |
+
try:
|
| 12 |
+
# Ensure database exists
|
| 13 |
+
if not os.path.exists(db_path):
|
| 14 |
+
init_sqlite_db(db_path)
|
| 15 |
+
|
| 16 |
+
# Connect to SQLite database
|
| 17 |
+
conn = sqlite3.connect(db_path)
|
| 18 |
+
cursor = conn.cursor()
|
| 19 |
+
|
| 20 |
+
# Sample data for customers
|
| 21 |
+
customers = [
|
| 22 |
+
('John Doe', 'john.doe@example.com', '555-123-4567', '123 Main St, Anytown, USA'),
|
| 23 |
+
('Jane Smith', 'jane.smith@example.com', '555-234-5678', '456 Oak Ave, Somewhere, USA'),
|
| 24 |
+
('Robert Johnson', 'robert.j@example.com', '555-345-6789', '789 Pine Rd, Nowhere, USA'),
|
| 25 |
+
('Emily Davis', 'emily.davis@example.com', '555-456-7890', '101 Maple Dr, Anywhere, USA'),
|
| 26 |
+
('Michael Wilson', 'michael.w@example.com', '555-567-8901', '202 Cedar Ln, Everywhere, USA'),
|
| 27 |
+
('Sarah Brown', 'sarah.b@example.com', '555-678-9012', '303 Birch Blvd, Somewhere, USA'),
|
| 28 |
+
('David Miller', 'david.m@example.com', '555-789-0123', '404 Elm St, Anytown, USA'),
|
| 29 |
+
('Jennifer Taylor', 'jennifer.t@example.com', '555-890-1234', '505 Walnut Ave, Nowhere, USA'),
|
| 30 |
+
('Christopher Anderson', 'chris.a@example.com', '555-901-2345', '606 Spruce Rd, Anywhere, USA'),
|
| 31 |
+
('Lisa Thomas', 'lisa.t@example.com', '555-012-3456', '707 Fir Dr, Everywhere, USA'),
|
| 32 |
+
('Daniel Jackson', 'daniel.j@example.com', '555-123-7890', '808 Pine St, Somewhere, USA'),
|
| 33 |
+
('Michelle White', 'michelle.w@example.com', '555-234-8901', '909 Oak Rd, Anytown, USA')
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
# Sample data for products
|
| 37 |
+
products = [
|
| 38 |
+
('Laptop Pro', 'High-performance laptop for professionals', 'Electronics', 1299.99, 50, 'LP-001'),
|
| 39 |
+
('Smartphone X', 'Latest smartphone with advanced features', 'Electronics', 899.99, 100, 'SP-001'),
|
| 40 |
+
('Office Chair', 'Ergonomic office chair', 'Furniture', 199.99, 30, 'OC-001'),
|
| 41 |
+
('Desk Lamp', 'LED desk lamp with adjustable brightness', 'Home', 49.99, 75, 'DL-001'),
|
| 42 |
+
('Coffee Maker', 'Programmable coffee maker', 'Appliances', 89.99, 40, 'CM-001'),
|
| 43 |
+
('Wireless Headphones', 'Noise-cancelling wireless headphones', 'Electronics', 149.99, 60, 'WH-001'),
|
| 44 |
+
('Tablet Mini', 'Compact tablet for on-the-go use', 'Electronics', 399.99, 45, 'TM-001'),
|
| 45 |
+
('External Hard Drive', '2TB external hard drive', 'Electronics', 129.99, 55, 'EH-001'),
|
| 46 |
+
('Wireless Mouse', 'Ergonomic wireless mouse', 'Electronics', 29.99, 80, 'WM-001'),
|
| 47 |
+
('Bluetooth Speaker', 'Portable Bluetooth speaker', 'Electronics', 79.99, 65, 'BS-001'),
|
| 48 |
+
('Monitor 27"', '27-inch 4K monitor', 'Electronics', 349.99, 35, 'MN-001'),
|
| 49 |
+
('Keyboard', 'Mechanical gaming keyboard', 'Electronics', 119.99, 70, 'KB-001'),
|
| 50 |
+
('Desk', 'Adjustable standing desk', 'Furniture', 299.99, 25, 'DK-001'),
|
| 51 |
+
('Bookshelf', 'Modern 5-tier bookshelf', 'Furniture', 149.99, 20, 'BS-002')
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
# Insert customers
|
| 55 |
+
cursor.executemany('''
|
| 56 |
+
INSERT INTO erp_customers (name, email, phone, address)
|
| 57 |
+
VALUES (?, ?, ?, ?)
|
| 58 |
+
''', customers)
|
| 59 |
+
|
| 60 |
+
# Insert products
|
| 61 |
+
cursor.executemany('''
|
| 62 |
+
INSERT INTO erp_products (product_name, description, category, price, stock_quantity, sku)
|
| 63 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 64 |
+
''', products)
|
| 65 |
+
|
| 66 |
+
# Get customer IDs for reference
|
| 67 |
+
cursor.execute('SELECT customer_id FROM erp_customers')
|
| 68 |
+
customer_ids = [row[0] for row in cursor.fetchall()]
|
| 69 |
+
|
| 70 |
+
# Get product IDs for reference
|
| 71 |
+
cursor.execute('SELECT product_id, price FROM erp_products')
|
| 72 |
+
product_data = cursor.fetchall()
|
| 73 |
+
product_ids = [row[0] for row in product_data]
|
| 74 |
+
product_prices = {row[0]: row[1] for row in product_data}
|
| 75 |
+
|
| 76 |
+
# Sample data for orders
|
| 77 |
+
orders = []
|
| 78 |
+
order_items = []
|
| 79 |
+
invoices = []
|
| 80 |
+
order_history = []
|
| 81 |
+
|
| 82 |
+
# Generate 15 orders
|
| 83 |
+
for i in range(1, 16):
|
| 84 |
+
# Randomly select a customer
|
| 85 |
+
customer_id = random.choice(customer_ids)
|
| 86 |
+
|
| 87 |
+
# Get customer address for shipping
|
| 88 |
+
cursor.execute('SELECT address FROM erp_customers WHERE customer_id = ?', (customer_id,))
|
| 89 |
+
address = cursor.fetchone()[0]
|
| 90 |
+
|
| 91 |
+
# Generate order date (within the last 60 days)
|
| 92 |
+
days_ago = random.randint(1, 60)
|
| 93 |
+
order_date = (datetime.datetime.now() - datetime.timedelta(days=days_ago)).strftime('%Y-%m-%d')
|
| 94 |
+
|
| 95 |
+
# Generate estimated delivery (3-10 days after order)
|
| 96 |
+
est_delivery = (datetime.datetime.now() - datetime.timedelta(days=days_ago) +
|
| 97 |
+
datetime.timedelta(days=random.randint(3, 10))).strftime('%Y-%m-%d')
|
| 98 |
+
|
| 99 |
+
# Determine if order has been delivered
|
| 100 |
+
delivered = random.random() > 0.3 # 70% chance of being delivered
|
| 101 |
+
actual_delivery = None
|
| 102 |
+
if delivered:
|
| 103 |
+
# Delivery occurred 0-2 days after estimated delivery
|
| 104 |
+
delivery_offset = random.randint(-1, 2) # Can be early or late
|
| 105 |
+
actual_delivery = (datetime.datetime.now() - datetime.timedelta(days=days_ago) +
|
| 106 |
+
datetime.timedelta(days=random.randint(3, 10) + delivery_offset)).strftime('%Y-%m-%d')
|
| 107 |
+
|
| 108 |
+
# Determine order status
|
| 109 |
+
if days_ago <= 1:
|
| 110 |
+
status = 'Processing'
|
| 111 |
+
elif days_ago <= 3:
|
| 112 |
+
status = 'Shipped'
|
| 113 |
+
elif delivered:
|
| 114 |
+
status = 'Delivered'
|
| 115 |
+
else:
|
| 116 |
+
status = random.choice(['Processing', 'Shipped', 'In Transit'])
|
| 117 |
+
|
| 118 |
+
# Determine payment status
|
| 119 |
+
payment_status = random.choice(['Paid', 'Pending', 'Paid', 'Paid']) # 75% chance of being paid
|
| 120 |
+
|
| 121 |
+
# Generate shipping and destination countries
|
| 122 |
+
shipping_country = random.choice([
|
| 123 |
+
'USA', 'Canada', 'UK', 'Germany', 'France', 'Australia',
|
| 124 |
+
'China', 'Japan', 'India', 'Brazil', 'Mexico', 'South Africa',
|
| 125 |
+
'Italy', 'Spain', 'Russia', 'South Korea', 'Singapore', 'UAE',
|
| 126 |
+
'Netherlands', 'Sweden'
|
| 127 |
+
])
|
| 128 |
+
destination_country = shipping_country # Usually the same
|
| 129 |
+
|
| 130 |
+
# Previous order (for some customers)
|
| 131 |
+
previous_order_id = None
|
| 132 |
+
if i > 5 and random.random() > 0.7: # 30% chance of having a previous order
|
| 133 |
+
previous_order_id = random.randint(1, i-1)
|
| 134 |
+
|
| 135 |
+
# Add to orders list
|
| 136 |
+
orders.append((
|
| 137 |
+
customer_id, order_date, 0, # Total amount will be updated later
|
| 138 |
+
status, previous_order_id, est_delivery, actual_delivery,
|
| 139 |
+
payment_status, address, shipping_country, destination_country
|
| 140 |
+
))
|
| 141 |
+
|
| 142 |
+
# Insert orders
|
| 143 |
+
cursor.executemany('''
|
| 144 |
+
INSERT INTO erp_orders (
|
| 145 |
+
customer_id, order_date, total_amount, status, previous_order_id,
|
| 146 |
+
estimated_delivery, actual_delivery, payment_status, shipping_address,
|
| 147 |
+
shipping_country, destination_country
|
| 148 |
+
)
|
| 149 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 150 |
+
''', orders)
|
| 151 |
+
|
| 152 |
+
# Get order IDs
|
| 153 |
+
cursor.execute('SELECT order_id FROM erp_orders')
|
| 154 |
+
order_ids = [row[0] for row in cursor.fetchall()]
|
| 155 |
+
|
| 156 |
+
# Generate order items for each order
|
| 157 |
+
for order_id in order_ids:
|
| 158 |
+
# Each order has 1-5 items
|
| 159 |
+
num_items = random.randint(1, 5)
|
| 160 |
+
order_total = 0
|
| 161 |
+
|
| 162 |
+
# Select random products for this order
|
| 163 |
+
selected_products = random.sample(product_ids, num_items)
|
| 164 |
+
|
| 165 |
+
for product_id in selected_products:
|
| 166 |
+
quantity = random.randint(1, 3)
|
| 167 |
+
unit_price = product_prices[product_id]
|
| 168 |
+
subtotal = quantity * unit_price
|
| 169 |
+
order_total += subtotal
|
| 170 |
+
|
| 171 |
+
# Add to order items list
|
| 172 |
+
order_items.append((order_id, product_id, quantity, unit_price, subtotal))
|
| 173 |
+
|
| 174 |
+
# Update order total
|
| 175 |
+
cursor.execute('UPDATE erp_orders SET total_amount = ? WHERE order_id = ?', (order_total, order_id))
|
| 176 |
+
|
| 177 |
+
# Generate invoice for paid orders
|
| 178 |
+
cursor.execute('SELECT payment_status FROM erp_orders WHERE order_id = ?', (order_id,))
|
| 179 |
+
payment_status = cursor.fetchone()[0]
|
| 180 |
+
|
| 181 |
+
if payment_status == 'Paid':
|
| 182 |
+
invoice_date = (datetime.datetime.now() - datetime.timedelta(days=random.randint(1, 30))).strftime('%Y-%m-%d')
|
| 183 |
+
due_date = (datetime.datetime.now() - datetime.timedelta(days=random.randint(1, 15))).strftime('%Y-%m-%d')
|
| 184 |
+
invoice_number = f'INV-{order_id}-{random.randint(1000, 9999)}'
|
| 185 |
+
|
| 186 |
+
invoices.append((order_id, invoice_date, order_total, 'Net 30', due_date, 1, invoice_number))
|
| 187 |
+
|
| 188 |
+
# Generate order history entries
|
| 189 |
+
# Initial status
|
| 190 |
+
order_history.append((
|
| 191 |
+
order_id,
|
| 192 |
+
(datetime.datetime.now() - datetime.timedelta(days=random.randint(50, 60))).strftime('%Y-%m-%d %H:%M:%S'),
|
| 193 |
+
'Order Created',
|
| 194 |
+
'New order placed',
|
| 195 |
+
'System'
|
| 196 |
+
))
|
| 197 |
+
|
| 198 |
+
# Processing status
|
| 199 |
+
order_history.append((
|
| 200 |
+
order_id,
|
| 201 |
+
(datetime.datetime.now() - datetime.timedelta(days=random.randint(40, 49))).strftime('%Y-%m-%d %H:%M:%S'),
|
| 202 |
+
'Processing',
|
| 203 |
+
'Order is being processed',
|
| 204 |
+
'System'
|
| 205 |
+
))
|
| 206 |
+
|
| 207 |
+
# Additional statuses based on current order status
|
| 208 |
+
cursor.execute('SELECT status FROM erp_orders WHERE order_id = ?', (order_id,))
|
| 209 |
+
current_status = cursor.fetchone()[0]
|
| 210 |
+
|
| 211 |
+
if current_status in ['Shipped', 'In Transit', 'Delivered']:
|
| 212 |
+
order_history.append((
|
| 213 |
+
order_id,
|
| 214 |
+
(datetime.datetime.now() - datetime.timedelta(days=random.randint(30, 39))).strftime('%Y-%m-%d %H:%M:%S'),
|
| 215 |
+
'Shipped',
|
| 216 |
+
'Order has been shipped',
|
| 217 |
+
'Shipping Dept'
|
| 218 |
+
))
|
| 219 |
+
|
| 220 |
+
if current_status in ['In Transit', 'Delivered']:
|
| 221 |
+
order_history.append((
|
| 222 |
+
order_id,
|
| 223 |
+
(datetime.datetime.now() - datetime.timedelta(days=random.randint(20, 29))).strftime('%Y-%m-%d %H:%M:%S'),
|
| 224 |
+
'In Transit',
|
| 225 |
+
'Order is in transit',
|
| 226 |
+
'Shipping Carrier'
|
| 227 |
+
))
|
| 228 |
+
|
| 229 |
+
if current_status == 'Delivered':
|
| 230 |
+
order_history.append((
|
| 231 |
+
order_id,
|
| 232 |
+
(datetime.datetime.now() - datetime.timedelta(days=random.randint(1, 19))).strftime('%Y-%m-%d %H:%M:%S'),
|
| 233 |
+
'Delivered',
|
| 234 |
+
'Order has been delivered',
|
| 235 |
+
'Shipping Carrier'
|
| 236 |
+
))
|
| 237 |
+
|
| 238 |
+
# Insert order items
|
| 239 |
+
cursor.executemany('''
|
| 240 |
+
INSERT INTO erp_order_items (order_id, product_id, quantity, unit_price, subtotal)
|
| 241 |
+
VALUES (?, ?, ?, ?, ?)
|
| 242 |
+
''', order_items)
|
| 243 |
+
|
| 244 |
+
# Insert invoices
|
| 245 |
+
cursor.executemany('''
|
| 246 |
+
INSERT INTO erp_invoices (order_id, invoice_date, amount, payment_terms, due_date, is_paid, invoice_number)
|
| 247 |
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
| 248 |
+
''', invoices)
|
| 249 |
+
|
| 250 |
+
# Insert order history
|
| 251 |
+
cursor.executemany('''
|
| 252 |
+
INSERT INTO erp_order_history (order_id, timestamp, status_change, notes, updated_by)
|
| 253 |
+
VALUES (?, ?, ?, ?, ?)
|
| 254 |
+
''', order_history)
|
| 255 |
+
|
| 256 |
+
# Sample data for global disruptions
|
| 257 |
+
disruption_types = [
|
| 258 |
+
'Natural Disaster', 'Political Unrest', 'Labor Strike',
|
| 259 |
+
'Transportation Issue', 'Customs Delay', 'Weather Event',
|
| 260 |
+
'Port Congestion', 'Regulatory Change', 'Supply Shortage',
|
| 261 |
+
'Infrastructure Failure', 'Security Threat', 'Health Crisis'
|
| 262 |
+
]
|
| 263 |
+
|
| 264 |
+
countries = [
|
| 265 |
+
'USA', 'Canada', 'UK', 'Germany', 'France', 'Australia',
|
| 266 |
+
'China', 'Japan', 'India', 'Brazil', 'Mexico', 'South Africa',
|
| 267 |
+
'Italy', 'Spain', 'Russia', 'South Korea', 'Singapore', 'UAE',
|
| 268 |
+
'Netherlands', 'Sweden'
|
| 269 |
+
]
|
| 270 |
+
|
| 271 |
+
global_disruptions = []
|
| 272 |
+
|
| 273 |
+
# Generate 15 global disruptions
|
| 274 |
+
for i in range(1, 16):
|
| 275 |
+
# Select random countries for source and destination
|
| 276 |
+
source_country = random.choice(countries)
|
| 277 |
+
# Ensure destination is different from source
|
| 278 |
+
destination_options = [c for c in countries if c != source_country]
|
| 279 |
+
destination_country = random.choice(destination_options)
|
| 280 |
+
|
| 281 |
+
# Select random disruption type
|
| 282 |
+
disruption_type = random.choice(disruption_types)
|
| 283 |
+
|
| 284 |
+
# Generate severity (1-5)
|
| 285 |
+
severity = random.randint(1, 5)
|
| 286 |
+
|
| 287 |
+
# Generate start date (within the last 90 days)
|
| 288 |
+
days_ago = random.randint(1, 90)
|
| 289 |
+
start_date = (datetime.datetime.now() - datetime.timedelta(days=days_ago)).strftime('%Y-%m-%d')
|
| 290 |
+
|
| 291 |
+
# Generate expected end date (1-30 days after start)
|
| 292 |
+
expected_days_duration = random.randint(1, 30)
|
| 293 |
+
expected_end_date = (datetime.datetime.now() - datetime.timedelta(days=days_ago) +
|
| 294 |
+
datetime.timedelta(days=expected_days_duration)).strftime('%Y-%m-%d')
|
| 295 |
+
|
| 296 |
+
# Determine if disruption has ended
|
| 297 |
+
is_ended = random.random() > 0.6 # 40% chance of being ended
|
| 298 |
+
actual_end_date = None
|
| 299 |
+
if is_ended:
|
| 300 |
+
# Actual end occurred 0-5 days after/before expected end
|
| 301 |
+
end_offset = random.randint(-5, 5)
|
| 302 |
+
actual_end_date = (datetime.datetime.now() - datetime.timedelta(days=days_ago) +
|
| 303 |
+
datetime.timedelta(days=expected_days_duration + end_offset)).strftime('%Y-%m-%d')
|
| 304 |
+
|
| 305 |
+
# Determine if disruption is active
|
| 306 |
+
is_active = not is_ended
|
| 307 |
+
|
| 308 |
+
# Generate impact hours based on severity
|
| 309 |
+
impact_hours = severity * random.randint(10, 48)
|
| 310 |
+
|
| 311 |
+
# Generate description
|
| 312 |
+
descriptions = {
|
| 313 |
+
'Natural Disaster': [
|
| 314 |
+
f"Severe flooding in {source_country} affecting shipments to {destination_country}",
|
| 315 |
+
f"Earthquake in {source_country} disrupting supply chain to {destination_country}",
|
| 316 |
+
f"Hurricane impacting shipping routes between {source_country} and {destination_country}"
|
| 317 |
+
],
|
| 318 |
+
'Political Unrest': [
|
| 319 |
+
f"Protests in {source_country} affecting exports to {destination_country}",
|
| 320 |
+
f"Trade dispute between {source_country} and {destination_country}",
|
| 321 |
+
f"Political tensions causing delays in shipments from {source_country} to {destination_country}"
|
| 322 |
+
],
|
| 323 |
+
'Labor Strike': [
|
| 324 |
+
f"Port workers strike in {source_country} affecting shipments to {destination_country}",
|
| 325 |
+
f"Transportation union strike impacting deliveries between {source_country} and {destination_country}",
|
| 326 |
+
f"Warehouse workers strike in {source_country} delaying orders to {destination_country}"
|
| 327 |
+
],
|
| 328 |
+
'Transportation Issue': [
|
| 329 |
+
f"Major highway closure between {source_country} and {destination_country}",
|
| 330 |
+
f"Shipping container shortage affecting routes from {source_country} to {destination_country}",
|
| 331 |
+
f"Fuel shortage in {source_country} impacting deliveries to {destination_country}"
|
| 332 |
+
],
|
| 333 |
+
'Customs Delay': [
|
| 334 |
+
f"New customs regulations in {destination_country} causing delays from {source_country}",
|
| 335 |
+
f"Increased inspection rates at {destination_country} border for goods from {source_country}",
|
| 336 |
+
f"Documentation issues for shipments from {source_country} to {destination_country}"
|
| 337 |
+
],
|
| 338 |
+
'Weather Event': [
|
| 339 |
+
f"Severe snowstorm in {source_country} delaying shipments to {destination_country}",
|
| 340 |
+
f"Fog at major ports in {source_country} affecting vessels bound for {destination_country}",
|
| 341 |
+
f"Extreme heat causing transportation issues between {source_country} and {destination_country}"
|
| 342 |
+
],
|
| 343 |
+
'Port Congestion': [
|
| 344 |
+
f"Backlog at {source_country} ports affecting shipments to {destination_country}",
|
| 345 |
+
f"Vessel scheduling issues at {source_country} ports for routes to {destination_country}",
|
| 346 |
+
f"Limited berthing capacity at {destination_country} ports for vessels from {source_country}"
|
| 347 |
+
],
|
| 348 |
+
'Regulatory Change': [
|
| 349 |
+
f"New import regulations in {destination_country} affecting goods from {source_country}",
|
| 350 |
+
f"Export restrictions in {source_country} for shipments to {destination_country}",
|
| 351 |
+
f"Changed documentation requirements between {source_country} and {destination_country}"
|
| 352 |
+
],
|
| 353 |
+
'Supply Shortage': [
|
| 354 |
+
f"Raw material shortage in {source_country} affecting production for {destination_country}",
|
| 355 |
+
f"Component shortage impacting products shipped from {source_country} to {destination_country}",
|
| 356 |
+
f"Limited availability of goods in {source_country} for export to {destination_country}"
|
| 357 |
+
],
|
| 358 |
+
'Infrastructure Failure': [
|
| 359 |
+
f"Bridge collapse on major route between {source_country} and {destination_country}",
|
| 360 |
+
f"Power outage in {source_country} affecting production for {destination_country}",
|
| 361 |
+
f"IT system failure impacting customs processing between {source_country} and {destination_country}"
|
| 362 |
+
],
|
| 363 |
+
'Security Threat': [
|
| 364 |
+
f"Increased piracy risk on shipping routes from {source_country} to {destination_country}",
|
| 365 |
+
f"Security concerns at {source_country} borders affecting shipments to {destination_country}",
|
| 366 |
+
f"Cybersecurity incident affecting logistics between {source_country} and {destination_country}"
|
| 367 |
+
],
|
| 368 |
+
'Health Crisis': [
|
| 369 |
+
f"Disease outbreak in {source_country} affecting workforce for exports to {destination_country}",
|
| 370 |
+
f"Quarantine requirements delaying shipments from {source_country} to {destination_country}",
|
| 371 |
+
f"Health screening causing delays at {destination_country} border for goods from {source_country}"
|
| 372 |
+
]
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
description = random.choice(descriptions.get(disruption_type, [f"{disruption_type} affecting shipments from {source_country} to {destination_country}"]))
|
| 376 |
+
|
| 377 |
+
# Add to global disruptions list
|
| 378 |
+
global_disruptions.append((
|
| 379 |
+
source_country, destination_country, disruption_type, severity,
|
| 380 |
+
start_date, expected_end_date, actual_end_date, is_active,
|
| 381 |
+
description, impact_hours
|
| 382 |
+
))
|
| 383 |
+
|
| 384 |
+
# Insert global disruptions
|
| 385 |
+
cursor.executemany('''
|
| 386 |
+
INSERT INTO live_global_disruptions (
|
| 387 |
+
source_country, destination_country, disruption_type, severity,
|
| 388 |
+
start_date, expected_end_date, actual_end_date, is_active,
|
| 389 |
+
description, impact_hours
|
| 390 |
+
)
|
| 391 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 392 |
+
''', global_disruptions)
|
| 393 |
+
|
| 394 |
+
# Commit changes and close connection
|
| 395 |
+
conn.commit()
|
| 396 |
+
conn.close()
|
| 397 |
+
|
| 398 |
+
print(f"Successfully populated ERP database with sample data.")
|
| 399 |
+
|
| 400 |
+
# Print summary of inserted data
|
| 401 |
+
print(f"Inserted {len(customers)} customers")
|
| 402 |
+
print(f"Inserted {len(products)} products")
|
| 403 |
+
print(f"Inserted {len(orders)} orders")
|
| 404 |
+
print(f"Inserted {len(order_items)} order items")
|
| 405 |
+
print(f"Inserted {len(invoices)} invoices")
|
| 406 |
+
print(f"Inserted {len(order_history)} order history entries")
|
| 407 |
+
print(f"Inserted {len(global_disruptions)} global disruptions")
|
| 408 |
+
|
| 409 |
+
return True
|
| 410 |
+
except Exception as e:
|
| 411 |
+
print(f"Error populating SQLite database: {str(e)}")
|
| 412 |
+
return False
|
| 413 |
+
|
| 414 |
+
if __name__ == "__main__":
|
| 415 |
+
populate_erp_db()
|
core/mcp_tools/reset_erp_db.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# reset_erp_db.py
|
| 2 |
+
import os
|
| 3 |
+
import pathlib
|
| 4 |
+
from erp_db_init import init_sqlite_db
|
| 5 |
+
from populate_erp_db import populate_erp_db
|
| 6 |
+
|
| 7 |
+
def reset_erp_db(db_path='./data/erp_db.sqlite'):
|
| 8 |
+
"""Reset the ERP database by deleting it and recreating it with fresh data."""
|
| 9 |
+
try:
|
| 10 |
+
print(f"Resetting ERP database at {db_path}...")
|
| 11 |
+
|
| 12 |
+
# Check if database file exists
|
| 13 |
+
if os.path.exists(db_path):
|
| 14 |
+
print(f"Removing existing database file...")
|
| 15 |
+
os.remove(db_path)
|
| 16 |
+
print(f"Database file removed.")
|
| 17 |
+
else:
|
| 18 |
+
print(f"No existing database file found.")
|
| 19 |
+
|
| 20 |
+
# Create database directory if it doesn't exist
|
| 21 |
+
db_dir = pathlib.Path(os.path.dirname(db_path))
|
| 22 |
+
db_dir.mkdir(exist_ok=True)
|
| 23 |
+
|
| 24 |
+
# Initialize the database
|
| 25 |
+
print(f"Initializing new database...")
|
| 26 |
+
init_result = init_sqlite_db(db_path)
|
| 27 |
+
if not init_result:
|
| 28 |
+
print(f"Failed to initialize database.")
|
| 29 |
+
return False
|
| 30 |
+
print(f"Database initialized successfully.")
|
| 31 |
+
|
| 32 |
+
# Populate the database with sample data
|
| 33 |
+
print(f"Populating database with sample data...")
|
| 34 |
+
populate_result = populate_erp_db(db_path)
|
| 35 |
+
if not populate_result:
|
| 36 |
+
print(f"Failed to populate database.")
|
| 37 |
+
return False
|
| 38 |
+
print(f"Database populated successfully.")
|
| 39 |
+
|
| 40 |
+
print(f"Database reset complete.")
|
| 41 |
+
return True
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"Error resetting database: {str(e)}")
|
| 44 |
+
return False
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
reset_erp_db()
|
data/erp_db.sqlite
ADDED
|
Binary file (98.3 kB). View file
|
|
|