unieai-mcp-accton-rfp 0.0.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ from dotenv import load_dotenv
2
+ from fastmcp import FastMCP
3
+ from openai import OpenAI
4
+ import requests
5
+ from openpyxl import load_workbook
6
+ import tempfile, os, json, inspect, re
7
+
8
+ load_dotenv()
9
+
10
+ # 初始化 MCP Server
11
+ app = FastMCP("ExcelProcessor")
12
+
13
+ # 初始化 OpenAI
14
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
15
+
16
+
17
+ # ======== 🔍 版本與功能檢查 ========
18
+ def _supports_responses_api():
19
+ return hasattr(client, "responses") and hasattr(client.responses, "create")
20
+
21
+ def _supports_response_format():
22
+ if not _supports_responses_api():
23
+ return False
24
+ try:
25
+ sig = inspect.signature(client.responses.create)
26
+ return "response_format" in sig.parameters
27
+ except Exception:
28
+ return False
29
+
30
+
31
+ # ======== 🧠 從文字中提取 JSON 區塊 ========
32
+ def _extract_json(text: str) -> dict:
33
+ """
34
+ 從 LLM 回覆中找出 JSON 區塊。
35
+ 支援包含說明文字、markdown、或額外符號的內容。
36
+ """
37
+ match = re.search(r"\{[\s\S]*\}", text)
38
+ if match:
39
+ json_part = match.group(0)
40
+ try:
41
+ return json.loads(json_part)
42
+ except json.JSONDecodeError:
43
+ pass
44
+ # 若解析失敗,保留原始文字
45
+ return {"Result": "解析錯誤", "Reference": text.strip()}
46
+
47
+
48
+ # ======== 🤖 呼叫 LLM,自動判斷版本 ========
49
+ def _call_openai(prompt: str) -> dict:
50
+ try:
51
+ if _supports_response_format():
52
+ print("1")
53
+ # ✅ 最新 SDK:支援 response_format
54
+ res = client.responses.create(
55
+ model="gpt-4o-mini",
56
+ input=prompt,
57
+ response_format={"type": "json_object"}
58
+ )
59
+ text = res.output[0].content[0].text
60
+ elif _supports_responses_api():
61
+ print("2")
62
+ # ⚠️ responses.create 存在但不支援 response_format
63
+ res = client.responses.create(model="gpt-4o-mini", input=prompt)
64
+ text = getattr(res.output[0].content[0], "text", str(res))
65
+ else:
66
+ print("3")
67
+ # ✅ 舊版 openai SDK
68
+ res = client.chat.completions.create(
69
+ model="gpt-4o-mini",
70
+ messages=[
71
+ {"role": "system", "content": "你是一個品質檢驗AI"},
72
+ {"role": "user", "content": prompt}
73
+ ]
74
+ )
75
+ text = res.choices[0].message.content.strip()
76
+
77
+ return _extract_json(text)
78
+ except Exception as e:
79
+ return {"Result": "Error", "Reference": f"LLM 呼叫失敗: {e}"}
80
+
81
+
82
+ # ======== 📊 Excel 處理邏輯 ========
83
+ def _process_excel_logic(url: str):
84
+ print(f"🟢 開始處理檔案: {url}")
85
+
86
+ # 下載或載入 Excel
87
+ if url.startswith("file:///"):
88
+ file_path = url.replace("file:///", "")
89
+ elif url.startswith("http://") or url.startswith("https://"):
90
+ resp = requests.get(url)
91
+ resp.raise_for_status()
92
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
93
+ tmp.write(resp.content)
94
+ file_path = tmp.name
95
+ else:
96
+ raise ValueError(f"❌ 不支援的檔案來源: {url}")
97
+
98
+ wb = load_workbook(file_path)
99
+ ws = wb.active
100
+
101
+ # 驗證欄位標題
102
+ header = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}
103
+ required = ["itemA", "itemB", "itemC", "itemD", "Result", "Reference"]
104
+ for r in required:
105
+ if r not in header:
106
+ raise ValueError(f"❌ 缺少欄位: {r}")
107
+
108
+ # 處理每一列資料
109
+ for row in ws.iter_rows(min_row=2, values_only=False):
110
+ a = row[header["itemA"] - 1].value or ""
111
+ b = row[header["itemB"] - 1].value or ""
112
+ c = row[header["itemC"] - 1].value or ""
113
+ d = row[header["itemD"] - 1].value or ""
114
+ if not any([a, b, c, d]):
115
+ continue
116
+
117
+ prompt = f"""
118
+ 你是一個品質檢驗AI,請根據以下項目輸出結果:
119
+
120
+ itemA: {a}
121
+ itemB: {b}
122
+ itemC: {c}
123
+ itemD: {d}
124
+
125
+ 請回傳 JSON:
126
+ {{"Result": "Conform / Half Conform / Not Conform", "Reference": "說明依據"}}
127
+ """
128
+
129
+ result_json = _call_openai(prompt)
130
+ ws.cell(row=row[0].row, column=header["Result"], value=result_json.get("Result"))
131
+ ws.cell(row=row[0].row, column=header["Reference"], value=result_json.get("Reference"))
132
+
133
+ # 儲存更新後的 Excel
134
+ out_path = os.path.join(tempfile.gettempdir(), f"updated_{os.path.basename(file_path)}")
135
+ wb.save(out_path)
136
+ print(f"✅ Excel 已處理完成,輸出檔案:{out_path}")
137
+
138
+ return {
139
+ "status": "success",
140
+ "output_path": out_path,
141
+ "message": "Excel 已更新完成"
142
+ }
143
+
144
+
145
+ # ======== 🔧 MCP 工具入口 ========
146
+ @app.tool()
147
+ def process_excel(url: str):
148
+ return _process_excel_logic(url)
149
+
150
+
151
+ # ======== 🚀 CLI 測試模式 : 單筆LLM請求 ========
152
+ if __name__ == "__main__":
153
+ test_path = r"C:\Users\Evan\Downloads\test_excel.xlsx"
154
+ #test_url = f"file:///{test_path}"
155
+
156
+ test_url = "https://sgp.cloud.appwrite.io/v1/storage/buckets/6904374b00056677a970/files/6904376a00173dabaf63/view?project=6901b22e0036150b66d3&mode=admin"
157
+
158
+ print("🚀 開始測試 Excel 檔案 ...")
159
+ result = _process_excel_logic(test_url)
160
+ print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -0,0 +1,245 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import re
6
+ import tempfile
7
+ import requests
8
+ from dotenv import load_dotenv
9
+ from fastmcp import FastMCP
10
+ from openpyxl import load_workbook
11
+
12
+ # LangChain modules
13
+ from langchain_openai import ChatOpenAI
14
+ from langchain_core.messages import HumanMessage, SystemMessage
15
+
16
+ # 初始化環境變數
17
+ load_dotenv()
18
+
19
+ APPWRITE_PROJECT_ID = os.getenv("APPWRITE_PROJECT_ID")
20
+ APPWRITE_API_KEY = os.getenv("APPWRITE_API_KEY") # 你剛說會用這個名字
21
+ APPWRITE_ENDPOINT = os.getenv("APPWRITE_ENDPOINT", "https://sgp.cloud.appwrite.io/v1")
22
+
23
+ # 初始化 MCP Server
24
+ app = FastMCP("ExcelProcessor")
25
+
26
+ # 設定 Logging
27
+ logging.basicConfig(level=logging.INFO)
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # 同時最多 10 個請求
31
+ semaphore = asyncio.Semaphore(10)
32
+
33
+ # ======== 🔧 初始化 LLM ========
34
+ llm = ChatOpenAI(
35
+ model=os.getenv("UNIEAI_MODEL"), # 你的模型名稱
36
+ base_url=os.getenv("UNIEAI_API_URL"), # 你的 API endpoint
37
+ api_key=os.getenv("UNIEAI_API_KEY"), # 從 .env 讀取金鑰
38
+ temperature=0,
39
+ )
40
+
41
+
42
+ def _parse_appwrite_url(url: str):
43
+ """
44
+ 從 Appwrite 檔案 URL 解析出 bucketId 和 fileId
45
+
46
+ 例如:
47
+ https://sgp.cloud.appwrite.io/v1/storage/buckets/6904374b00056677a970/files/691894e30027b282e721/view?project=...
48
+ 會解析成:
49
+ bucketId = 6904374b00056677a970
50
+ fileId = 691894e30027b282e721
51
+ """
52
+ # 只看 path 中的 /storage/buckets/{bucketId}/files/{fileId}
53
+ pattern = r"/storage/buckets/([^/]+)/files/([^/]+)"
54
+ m = re.search(pattern, url)
55
+ if not m:
56
+ return None, None
57
+ bucket_id, file_id = m.group(1), m.group(2)
58
+ return bucket_id, file_id
59
+
60
+
61
+
62
+
63
+ # ======== 🧠 從文字中提取 JSON 區塊 ========
64
+ def _extract_json(text: str) -> dict:
65
+ match = re.search(r"\{[\s\S]*\}", text)
66
+ if match:
67
+ try:
68
+ return json.loads(match.group(0))
69
+ except json.JSONDecodeError:
70
+ pass
71
+ return {"Result": "解析錯誤", "Reference": text.strip()}
72
+
73
+ # ======== 🤖 呼叫 LLM ========
74
+ async def _call_llm(prompt: str, user_message: str, row_id: int) -> dict:
75
+ try:
76
+ async with semaphore:
77
+ logger.info(f"🔄 發送請求給 LLM (ID: {row_id})")
78
+
79
+ response = await llm.ainvoke([
80
+ SystemMessage(content= prompt ),
81
+ HumanMessage(content= user_message)
82
+ ])
83
+
84
+ text = response.content.strip()
85
+ logger.info(f"🔄 text : {text}")
86
+ logger.info(f"🔄 _extract_json(text) : _extract_json(text)")
87
+ return _extract_json(text)
88
+
89
+ except Exception as e:
90
+ logger.error(f"❌ LLM 呼叫失敗 (ID: {row_id}): {e}")
91
+ return {"Result": "Error", "Reference": f"LLM 呼叫失敗: {e}"}
92
+
93
+ # ======== 📊 Excel 處理邏輯 ========
94
+ async def _process_excel_logic(url: str):
95
+ print(f"🟢 開始處理檔案: {url}")
96
+
97
+ # 判斷來源類型 & 下載或載入 Excel
98
+ source_type = None # "local" / "appwrite" / "remote_readonly"
99
+ local_path = None # 若是本機檔案就記錄原始路徑
100
+
101
+ if url.startswith("file:///"):
102
+ # 本機檔案,直接用原路徑開啟,等等也覆寫回去
103
+ local_path = url.replace("file:///", "")
104
+ file_path = local_path
105
+ source_type = "local"
106
+
107
+ elif url.startswith(("http://", "https://")):
108
+ # 遠端檔案,先下載到暫存檔再處理
109
+ resp = requests.get(url)
110
+ resp.raise_for_status()
111
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
112
+ tmp.write(resp.content)
113
+ file_path = tmp.name
114
+
115
+ # 嘗試判斷是不是 Appwrite 的 storage 檔案 URL
116
+ bucket_id, file_id = _parse_appwrite_url(url)
117
+ if bucket_id and file_id:
118
+ source_type = "appwrite"
119
+ else:
120
+ source_type = "remote_readonly" # 一般 HTTP,只能讀不能寫回
121
+
122
+ else:
123
+ raise ValueError(f"❌ 不支援的檔案來源: {url}")
124
+
125
+ # 開啟 Excel
126
+ wb = load_workbook(file_path)
127
+ ws = wb.active
128
+
129
+ # 驗證欄位
130
+ header = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}
131
+ required = ["itemA", "itemB", "itemC", "itemD", "Result", "Reference"]
132
+ for r in required:
133
+ if r not in header:
134
+ raise ValueError(f"❌ 缺少欄位: {r}")
135
+
136
+ # 準備 LLM 任務
137
+ tasks = []
138
+ for row in ws.iter_rows(min_row=2, values_only=False):
139
+ row_id = row[0].row
140
+ a, b, c, d = [row[header[k]-1].value or "" for k in ["itemA", "itemB", "itemC", "itemD"]]
141
+ if not any([a, b, c, d]):
142
+ continue
143
+
144
+ prompt = f"""
145
+
146
+
147
+ """
148
+
149
+ user_message = f"""
150
+
151
+
152
+ {a}
153
+
154
+
155
+ """
156
+
157
+ print(f"🔄 prompt : {prompt}")
158
+ print(f"🔄 user_message : {user_message}")
159
+
160
+ tasks.append(_call_llm(prompt, user_message, row_id))
161
+
162
+ # 並發執行 LLM
163
+ results = await asyncio.gather(*tasks)
164
+
165
+ # 更新 Excel
166
+ for row, result_json in zip(ws.iter_rows(min_row=2, values_only=False), results):
167
+ ws.cell(row=row[0].row, column=header["Result"], value=result_json.get("Result"))
168
+ ws.cell(row=row[0].row, column=header["Reference"], value=result_json.get("Reference"))
169
+
170
+ # ==========================
171
+ # 🔥 回寫到「原始來源」
172
+ # ==========================
173
+
174
+ # 1) 本機檔案:直接覆寫原檔
175
+ if source_type == "local":
176
+ wb.save(local_path)
177
+ print(f"✅ Excel 已覆寫回本機檔案: {local_path}")
178
+ return {
179
+ "status": "success",
180
+ "location_type": "local",
181
+ "output_path": local_path,
182
+ "message": "Excel 已成功覆寫回原本機檔案"
183
+ }
184
+
185
+ # 2) Appwrite:使用 PUT /storage/buckets/{bucketId}/files/{fileId}
186
+ if source_type == "appwrite":
187
+ if not APPWRITE_PROJECT_ID or not APPWRITE_API_KEY:
188
+ raise RuntimeError("❌ APPWRITE_PROJECT_ID 或 APPWRITE_API_KEY 未設定,無法回寫到 Appwrite")
189
+
190
+ bucket_id, file_id = _parse_appwrite_url(url)
191
+ upload_url = f"{APPWRITE_ENDPOINT}/storage/buckets/{bucket_id}/files/{file_id}"
192
+
193
+ # 先存成暫存檔再 PUT
194
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp_out:
195
+ wb.save(tmp_out.name)
196
+ tmp_out_path = tmp_out.name
197
+
198
+ headers = {
199
+ "X-Appwrite-Project": APPWRITE_PROJECT_ID,
200
+ "X-Appwrite-Key": APPWRITE_API_KEY,
201
+ "Content-Type": "application/octet-stream",
202
+ }
203
+
204
+ print(f"📤 開始 PUT 覆寫到 Appwrite: {upload_url}")
205
+ with open(tmp_out_path, "rb") as f:
206
+ put_resp = requests.put(upload_url, headers=headers, data=f)
207
+ put_resp.raise_for_status()
208
+
209
+ print("✅ Excel 已成功覆寫回 Appwrite")
210
+ return {
211
+ "status": "success",
212
+ "location_type": "appwrite",
213
+ "output_url": upload_url,
214
+ "message": "Excel 已成功覆寫回 Appwrite 檔案"
215
+ }
216
+
217
+ # 3) 其他 http/https(不知道怎麼寫回)→ 只好當成處理後檔案給你路徑
218
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp_out:
219
+ wb.save(tmp_out.name)
220
+ out_path = tmp_out.name
221
+
222
+ print(f"⚠️ 遠端 URL 非 Appwrite,無法自動寫回,只能輸出更新後檔案路徑: {out_path}")
223
+ return {
224
+ "status": "success",
225
+ "location_type": "unknown_remote",
226
+ "output_path": out_path,
227
+ "message": "遠端 URL 非 Appwrite 格式,已在本機暫存路徑產生更新後檔案"
228
+ }
229
+
230
+
231
+ # ======== 🔧 MCP 工具入口 ========
232
+ @app.tool()
233
+ async def process_excel(url: str):
234
+ return await _process_excel_logic(url)
235
+
236
+ # ======== 🚀 CLI 測試模式 ========
237
+ if __name__ == "__main__":
238
+ test_path = r"C:\Users\Evan\Downloads\test_excel.xlsx"
239
+ #test_url = f"file:///{test_path}"
240
+
241
+ test_url = "https://sgp.cloud.appwrite.io/v1/storage/buckets/6904374b00056677a970/files/691894e30027b282e721/view?project=6901b22e0036150b66d3&mode=admin"
242
+
243
+
244
+ print("🚀 開始測試 Excel 檔案 ...")
245
+ asyncio.run(_process_excel_logic(test_url))
@@ -0,0 +1,171 @@
1
+ import asyncio
2
+ import aiohttp
3
+ from dotenv import load_dotenv
4
+ from fastmcp import FastMCP
5
+ from openai import OpenAI
6
+ import requests
7
+ from openpyxl import load_workbook
8
+ import tempfile, os, json, inspect, re
9
+
10
+ load_dotenv()
11
+
12
+ # 初始化 MCP Server
13
+ app = FastMCP("ExcelProcessor")
14
+
15
+ # 初始化 OpenAI
16
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
17
+
18
+ # 限制同時最多處理 10 個請求
19
+ MAX_CONCURRENT_REQUESTS = 10
20
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
21
+
22
+ # ======== 🔍 版本與功能檢查 ========
23
+ def _supports_responses_api():
24
+ return hasattr(client, "responses") and hasattr(client.responses, "create")
25
+
26
+ def _supports_response_format():
27
+ if not _supports_responses_api():
28
+ return False
29
+ try:
30
+ sig = inspect.signature(client.responses.create)
31
+ return "response_format" in sig.parameters
32
+ except Exception:
33
+ return False
34
+
35
+
36
+ # ======== 🧠 從文字中提取 JSON 區塊 ========
37
+ def _extract_json(text: str) -> dict:
38
+ """
39
+ 從 LLM 回覆中找出 JSON 區塊。
40
+ 支援包含說明文字、markdown、或額外符號的內容。
41
+ """
42
+ match = re.search(r"\{[\s\S]*\}", text)
43
+ if match:
44
+ json_part = match.group(0)
45
+ try:
46
+ return json.loads(json_part)
47
+ except json.JSONDecodeError:
48
+ pass
49
+ # 若解析失敗,保留原始文字
50
+ return {"Result": "解析錯誤", "Reference": text.strip()}
51
+
52
+
53
+ # ======== 🤖 呼叫 LLM,自動判斷版本 ========
54
+ async def _call_openai(prompt: str, session: aiohttp.ClientSession) -> dict:
55
+ """根據環境自動選擇正確 API 呼叫,並自動從文字中提取 JSON"""
56
+ try:
57
+ async with semaphore: # 🔐 限制同時處理的請求數量
58
+ if _supports_response_format():
59
+ # ✅ 最新 SDK:支援 response_format
60
+ async with session.post("https://api.openai.com/v1/completions", json={
61
+ "model": "gpt-4o-mini",
62
+ "prompt": prompt,
63
+ "response_format": {"type": "json_object"}
64
+ }) as response:
65
+ text = await response.json()
66
+ elif _supports_responses_api():
67
+ # ⚠️ responses.create 存在但不支援 response_format
68
+ async with session.post("https://api.openai.com/v1/completions", json={
69
+ "model": "gpt-4o-mini",
70
+ "prompt": prompt
71
+ }) as response:
72
+ text = await response.json()
73
+ else:
74
+ # ✅ 舊版 openai SDK
75
+ async with session.post("https://api.openai.com/v1/chat/completions", json={
76
+ "model": "gpt-4o-mini",
77
+ "messages": [{"role": "system", "content": "你是一個品質檢驗AI"}, {"role": "user", "content": prompt}]
78
+ }) as response:
79
+ text = await response.json()
80
+
81
+ return _extract_json(text['choices'][0]['message']['content'])
82
+ except Exception as e:
83
+ return {"Result": "Error", "Reference": f"LLM 呼叫失敗: {e}"}
84
+
85
+
86
+ # ======== 📊 Excel 處理邏輯 ========
87
+ async def _process_excel_logic(url: str):
88
+ print(f"🟢 開始處理檔案: {url}")
89
+
90
+ # 下載或載入 Excel
91
+ if url.startswith("file:///"):
92
+ file_path = url.replace("file:///", "")
93
+ elif url.startswith("http://") or url.startswith("https://"):
94
+ resp = requests.get(url)
95
+ resp.raise_for_status()
96
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
97
+ tmp.write(resp.content)
98
+ file_path = tmp.name
99
+ else:
100
+ raise ValueError(f"❌ 不支援的檔案來源: {url}")
101
+
102
+ wb = load_workbook(file_path)
103
+ ws = wb.active
104
+
105
+ # 驗證欄位標題
106
+ header = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}
107
+ required = ["itemA", "itemB", "itemC", "itemD", "Result", "Reference"]
108
+ for r in required:
109
+ if r not in header:
110
+ raise ValueError(f"❌ 缺少欄位: {r}")
111
+
112
+ # 异步處理每一列資料
113
+ async with aiohttp.ClientSession() as session:
114
+ tasks = []
115
+ for row in ws.iter_rows(min_row=2, values_only=False):
116
+ a = row[header["itemA"] - 1].value or ""
117
+ b = row[header["itemB"] - 1].value or ""
118
+ c = row[header["itemC"] - 1].value or ""
119
+ d = row[header["itemD"] - 1].value or ""
120
+ if not any([a, b, c, d]):
121
+ continue
122
+
123
+ prompt = f"""
124
+ 你是一個品質檢驗AI,請根據以下項目輸出結果:
125
+
126
+ itemA: {a}
127
+ itemB: {b}
128
+ itemC: {c}
129
+ itemD: {d}
130
+
131
+ 請回傳 JSON:
132
+ {{"Result": "Conform / Half Conform / Not Conform", "Reference": "說明依據"}}
133
+ """
134
+ # 在這裡加入每個呼叫 LLM 的異步任務
135
+ tasks.append(_call_openai(prompt, session))
136
+
137
+ # 批次執行,最多同時 10 個請求
138
+ responses = await asyncio.gather(*tasks)
139
+
140
+ # 更新 Excel
141
+ for row, result_json in zip(ws.iter_rows(min_row=2, values_only=False), responses):
142
+ ws.cell(row=row[0].row, column=header["Result"], value=result_json.get("Result"))
143
+ ws.cell(row=row[0].row, column=header["Reference"], value=result_json.get("Reference"))
144
+
145
+ # 儲存更新後的 Excel
146
+ out_path = os.path.join(tempfile.gettempdir(), f"updated_{os.path.basename(file_path)}")
147
+ wb.save(out_path)
148
+ print(f"✅ Excel 已處理完成,輸出檔案:{out_path}")
149
+
150
+ return {
151
+ "status": "success",
152
+ "output_path": out_path,
153
+ "message": "Excel 已更新完成"
154
+ }
155
+
156
+
157
+ # ======== 🔧 MCP 工具入口 ========
158
+ @app.tool()
159
+ async def process_excel(url: str):
160
+ return await _process_excel_logic(url)
161
+
162
+
163
+ # ======== 🚀 CLI 測試模式 : 批次一次10筆LLM請求========
164
+ if __name__ == "__main__":
165
+ test_path = r"C:\Users\Evan\Downloads\test_excel.xlsx"
166
+ #test_url = f"file:///{test_path}"
167
+
168
+ test_url = "https://sgp.cloud.appwrite.io/v1/storage/buckets/6904374b00056677a970/files/6904376a00173dabaf63/view?project=6901b22e0036150b66d3&mode=admin"
169
+
170
+ print("🚀 開始測試 Excel 檔案 ...")
171
+ asyncio.run(_process_excel_logic(test_url))