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.
- unieai_mcp_accton_rfp/__init__.py +0 -0
- unieai_mcp_accton_rfp/server.py +375 -0
- unieai_mcp_accton_rfp/server_old.py +189 -0
- unieai_mcp_accton_rfp/server_v0.py +337 -0
- unieai_mcp_accton_rfp/server_v02.py +327 -0
- unieai_mcp_accton_rfp/server_v1.py +160 -0
- unieai_mcp_accton_rfp/server_v2.py +245 -0
- unieai_mcp_accton_rfp/server_v3.py +171 -0
- unieai_mcp_accton_rfp/server_v4.py +373 -0
- unieai_mcp_accton_rfp/server_v6.py +349 -0
- unieai_mcp_accton_rfp/server_v7.py +367 -0
- unieai_mcp_accton_rfp/test.py +108 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/METADATA +30 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/RECORD +18 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/WHEEL +5 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/entry_points.txt +2 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/licenses/LICENSE +7 -0
- unieai_mcp_accton_rfp-0.0.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Any, Dict, Tuple, List, Optional
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
from openpyxl import load_workbook
|
|
14
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
15
|
+
|
|
16
|
+
# LangChain (1.x API)
|
|
17
|
+
from langchain_openai import ChatOpenAI
|
|
18
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
19
|
+
|
|
20
|
+
# ==============================
|
|
21
|
+
# 🎛 Environment & Logging
|
|
22
|
+
# ==============================
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
|
|
26
|
+
logging.basicConfig(level=logging.INFO)
|
|
27
|
+
logger = logging.getLogger("ExcelProcessor")
|
|
28
|
+
|
|
29
|
+
app = FastMCP("ExcelProcessor")
|
|
30
|
+
semaphore = asyncio.Semaphore(10)
|
|
31
|
+
|
|
32
|
+
# LLM 初始化 (LangChain 1.x)
|
|
33
|
+
llm = ChatOpenAI(
|
|
34
|
+
model=os.getenv("UNIEAI_MODEL"),
|
|
35
|
+
base_url=os.getenv("UNIEAI_API_URL"),
|
|
36
|
+
api_key=os.getenv("UNIEAI_API_KEY"),
|
|
37
|
+
temperature=0,
|
|
38
|
+
max_tokens=32768
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Appwrite ENV
|
|
42
|
+
APPWRITE_PROJECT_ID = os.getenv("APPWRITE_PROJECT_ID")
|
|
43
|
+
APPWRITE_API_KEY = os.getenv("APPWRITE_API_KEY")
|
|
44
|
+
APPWRITE_ENDPOINT = os.getenv("APPWRITE_ENDPOINT", "https://sgp.cloud.appwrite.io/v1")
|
|
45
|
+
|
|
46
|
+
BATCH_SIZE = 5 # 可自由調整
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ==============================
|
|
50
|
+
# 🧩 Helper Functions
|
|
51
|
+
# ==============================
|
|
52
|
+
|
|
53
|
+
def _extract_json(text: str) -> Dict[str, Any]:
|
|
54
|
+
"""擷取 JSON 區塊"""
|
|
55
|
+
match = re.search(r"\{[\s\S]*\}", text)
|
|
56
|
+
if match:
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(match.group(0))
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.warning(f"JSON 解析失敗: {e}")
|
|
61
|
+
return {"Result": "解析錯誤", "Reference": text.strip()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_appwrite_url(url: str) -> Tuple[Optional[str], Optional[str]]:
|
|
65
|
+
pattern = r"/storage/buckets/([^/]+)/files/([^/]+)"
|
|
66
|
+
m = re.search(pattern, url)
|
|
67
|
+
if not m:
|
|
68
|
+
return None, None
|
|
69
|
+
return m.group(1), m.group(2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _generate_new_filename(original_name: str) -> str:
|
|
73
|
+
base, ext = os.path.splitext(original_name)
|
|
74
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
75
|
+
return f"{base}_processed_{timestamp}{ext}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ==============================
|
|
79
|
+
# 🤖 LLM Logic(新增兩階段)
|
|
80
|
+
# ==============================
|
|
81
|
+
|
|
82
|
+
async def _call_llm_raw(prompt: str, user_message: str):
|
|
83
|
+
"""返回 LLM 純文字內容"""
|
|
84
|
+
logger.info(f"🟢 _call_llm_raw : {prompt}, {user_message}")
|
|
85
|
+
try:
|
|
86
|
+
async with semaphore:
|
|
87
|
+
response = await llm.ainvoke([
|
|
88
|
+
SystemMessage(content=prompt),
|
|
89
|
+
HumanMessage(content=user_message)
|
|
90
|
+
])
|
|
91
|
+
return (response.content or "").strip()
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return f"LLM Error: {e}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_result_json(text: str):
|
|
97
|
+
"""解析第二階段 JSON"""
|
|
98
|
+
try:
|
|
99
|
+
return json.loads(re.search(r"\{[\s\S]*\}", text).group(0))
|
|
100
|
+
except:
|
|
101
|
+
return {"Result": "Error"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ==============================
|
|
105
|
+
# 📘 Prompt 建構(新:兩個 prompt)
|
|
106
|
+
# ==============================
|
|
107
|
+
|
|
108
|
+
def _build_reference_prompt() -> str:
|
|
109
|
+
return """
|
|
110
|
+
你是一位嚴謹的產品經理助理,專門負責將內部產品規格(知識庫)與客戶的需求單(RFP)進行比對和符合性分析。
|
|
111
|
+
|
|
112
|
+
**任務指示:**
|
|
113
|
+
1. 你將收到客戶的產品需求單 (RFP) 作為輸入。
|
|
114
|
+
2. 你的知識庫已包含你公司產品的完整說明文件。
|
|
115
|
+
3. 請仔細閱讀 RFP 中的每一條具體需求,並利用你的產品知識庫內容進行嚴格比對。
|
|
116
|
+
|
|
117
|
+
**比對規則:**
|
|
118
|
+
* **Conform (完全符合):** 公司的產品規格能**完整且無條件地**滿足 RFP 中的該項需求。
|
|
119
|
+
* **Half Conform (部分符合):** 公司的產品規格**只能滿足** RFP 中該項需求的**部分內容**,或者需要透過**變通、額外配置或未來規劃**才能滿足。
|
|
120
|
+
* **Not Conform (不符合):** 公司的產品規格**無法滿足** RFP 中的該項需求。
|
|
121
|
+
|
|
122
|
+
**輸出格式要求:**
|
|
123
|
+
你必須以條列式清晰地輸出分析結果,**每一條結果必須包含**:
|
|
124
|
+
1. RFP 中的**原始需求描述** (簡短摘錄或編號)。
|
|
125
|
+
2. **符合程度** (只能是:Conform, Half Conform, Not Conform 三者之一)。
|
|
126
|
+
3. **參考依據** (說明做出判斷的依據,需明確引用知識庫中**相關產品說明**的關鍵資訊或段落,例如:知識庫中「功能A」的描述支持此判斷)。
|
|
127
|
+
|
|
128
|
+
請針對 RFP 中的每一條主要需求逐一進行分析。
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _build_result_prompt() -> str:
|
|
133
|
+
return """
|
|
134
|
+
請依據以下 Reference 文本,判斷其符合性:
|
|
135
|
+
- Conform:完全符合
|
|
136
|
+
- Half Conform:部分符合
|
|
137
|
+
- Not Conform:不符合
|
|
138
|
+
|
|
139
|
+
請僅輸出以下 JSON 格式:
|
|
140
|
+
{
|
|
141
|
+
"Result": "Conform / Half Conform / Not Conform"
|
|
142
|
+
}
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _build_user_message(a: str, b: str, c: str, d: str) -> str:
|
|
147
|
+
logger.info(f"🟢 _build_user_message : {a}, {b}, {c}, {d}")
|
|
148
|
+
return f"""
|
|
149
|
+
{a}, {b}, {c}, {d}
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def chunk_list(data, size):
|
|
154
|
+
"""將 list 切成固定大小的區塊"""
|
|
155
|
+
for i in range(0, len(data), size):
|
|
156
|
+
yield data[i:i + size]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ==============================
|
|
164
|
+
# 📊 Excel Processing Core
|
|
165
|
+
# ==============================
|
|
166
|
+
|
|
167
|
+
async def _process_excel_logic(url: str) -> Dict[str, Any]:
|
|
168
|
+
logger.info(f"🟢 開始處理 Excel:{url}")
|
|
169
|
+
|
|
170
|
+
# -------------------------
|
|
171
|
+
# Step 1: Download / Load
|
|
172
|
+
# -------------------------
|
|
173
|
+
source_type = ""
|
|
174
|
+
local_path = None
|
|
175
|
+
appwrite_info = (None, None)
|
|
176
|
+
bucket_id = None
|
|
177
|
+
|
|
178
|
+
if url.startswith("file:///"):
|
|
179
|
+
local_path = url.replace("file:///", "")
|
|
180
|
+
file_path = local_path
|
|
181
|
+
source_type = "local"
|
|
182
|
+
|
|
183
|
+
elif url.startswith("http"):
|
|
184
|
+
resp = requests.get(url)
|
|
185
|
+
resp.raise_for_status()
|
|
186
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
|
|
187
|
+
tmp.write(resp.content)
|
|
188
|
+
file_path = tmp.name
|
|
189
|
+
|
|
190
|
+
bucket_id, file_id = _parse_appwrite_url(url)
|
|
191
|
+
if bucket_id:
|
|
192
|
+
source_type = "appwrite"
|
|
193
|
+
appwrite_info = (bucket_id, file_id)
|
|
194
|
+
else:
|
|
195
|
+
source_type = "remote_readonly"
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
raise ValueError("❌ 不支援檔案來源")
|
|
199
|
+
|
|
200
|
+
# -------------------------
|
|
201
|
+
# Step 2: Open Excel
|
|
202
|
+
# -------------------------
|
|
203
|
+
wb = load_workbook(file_path)
|
|
204
|
+
ws = wb.active
|
|
205
|
+
|
|
206
|
+
header = {cell.value: idx for idx, cell in enumerate(ws[1], 1)}
|
|
207
|
+
for col in ["itemA", "itemB", "itemC", "itemD", "Result", "Reference"]:
|
|
208
|
+
if col not in header:
|
|
209
|
+
raise ValueError(f"❌ Excel 缺少欄位:{col}")
|
|
210
|
+
|
|
211
|
+
# -------------------------
|
|
212
|
+
# Step 3: Two-stage LLM (Batch + Parallel)
|
|
213
|
+
# -------------------------
|
|
214
|
+
|
|
215
|
+
rows_for_llm = []
|
|
216
|
+
|
|
217
|
+
for row in ws.iter_rows(min_row=2, values_only=False):
|
|
218
|
+
if any([cell.value for cell in row]):
|
|
219
|
+
rows_for_llm.append(row)
|
|
220
|
+
|
|
221
|
+
reference_prompt = _build_reference_prompt()
|
|
222
|
+
result_prompt = _build_result_prompt()
|
|
223
|
+
|
|
224
|
+
# 批次處理
|
|
225
|
+
for batch_rows in chunk_list(rows_for_llm, BATCH_SIZE):
|
|
226
|
+
|
|
227
|
+
# -------- Build input messages for this batch --------
|
|
228
|
+
user_messages = []
|
|
229
|
+
for row in batch_rows:
|
|
230
|
+
a = row[header["itemA"] - 1].value or ""
|
|
231
|
+
b = row[header["itemB"] - 1].value or ""
|
|
232
|
+
c = row[header["itemC"] - 1].value or ""
|
|
233
|
+
d = row[header["itemD"] - 1].value or ""
|
|
234
|
+
|
|
235
|
+
user_messages.append(_build_user_message(str(a), str(b), str(c), str(d)))
|
|
236
|
+
|
|
237
|
+
# -------- Stage 1: Reference (parallel inside batch) --------
|
|
238
|
+
ref_tasks = [
|
|
239
|
+
_call_llm_raw(reference_prompt, msg)
|
|
240
|
+
for msg in user_messages
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
reference_results = await asyncio.gather(*ref_tasks)
|
|
244
|
+
|
|
245
|
+
# 寫入每列 Reference
|
|
246
|
+
for row, ref_text in zip(batch_rows, reference_results):
|
|
247
|
+
r = row[0].row
|
|
248
|
+
ws.cell(r, header["Reference"], ref_text)
|
|
249
|
+
|
|
250
|
+
# -------- Stage 2: Result 判斷 (parallel inside batch) --------
|
|
251
|
+
result_tasks = [
|
|
252
|
+
_call_llm_raw(result_prompt, ref_text)
|
|
253
|
+
for ref_text in reference_results
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
raw_result_outputs = await asyncio.gather(*result_tasks)
|
|
257
|
+
|
|
258
|
+
# 寫入 Result
|
|
259
|
+
for row, raw_result in zip(batch_rows, raw_result_outputs):
|
|
260
|
+
r = row[0].row
|
|
261
|
+
parsed = _extract_result_json(raw_result)
|
|
262
|
+
ws.cell(r, header["Result"], parsed.get("Result", "Error"))
|
|
263
|
+
|
|
264
|
+
logger.info(f"🔥 完成一批 {len(batch_rows)} 筆 LLM 分析")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# -------------------------
|
|
269
|
+
# Step 4: Save local debug copy
|
|
270
|
+
# -------------------------
|
|
271
|
+
local_debug_dir = r"D:\TempExcelDebug"
|
|
272
|
+
os.makedirs(local_debug_dir, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
local_debug_filename = _generate_new_filename("debug_output.xlsx")
|
|
275
|
+
local_debug_path = os.path.join(local_debug_dir, local_debug_filename)
|
|
276
|
+
|
|
277
|
+
wb.save(local_debug_path)
|
|
278
|
+
logger.info(f"📝 本機 debug 檔案已輸出:{local_debug_path}")
|
|
279
|
+
|
|
280
|
+
# -------------------------
|
|
281
|
+
# Step 5: Write back according to source
|
|
282
|
+
# -------------------------
|
|
283
|
+
|
|
284
|
+
# local
|
|
285
|
+
if source_type == "local":
|
|
286
|
+
wb.save(local_path)
|
|
287
|
+
return {
|
|
288
|
+
"status": "success",
|
|
289
|
+
"location_type": "local",
|
|
290
|
+
"output_path": local_path
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Appwrite
|
|
294
|
+
if source_type == "appwrite":
|
|
295
|
+
bucket_id, _ = appwrite_info
|
|
296
|
+
|
|
297
|
+
tmp_out_path = os.path.join(
|
|
298
|
+
tempfile.gettempdir(),
|
|
299
|
+
_generate_new_filename("upload.xlsx")
|
|
300
|
+
)
|
|
301
|
+
wb.save(tmp_out_path)
|
|
302
|
+
|
|
303
|
+
new_file_id = f"processed_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
304
|
+
new_file_name = f"{new_file_id}.xlsx"
|
|
305
|
+
|
|
306
|
+
upload_url = f"{APPWRITE_ENDPOINT}/storage/buckets/{bucket_id}/files"
|
|
307
|
+
|
|
308
|
+
headers = {
|
|
309
|
+
"X-Appwrite-Project": APPWRITE_PROJECT_ID,
|
|
310
|
+
"X-Appwrite-Key": APPWRITE_API_KEY,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
files = {
|
|
314
|
+
"file": (
|
|
315
|
+
new_file_name,
|
|
316
|
+
open(tmp_out_path, "rb"),
|
|
317
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
data = { "fileId": new_file_id }
|
|
322
|
+
|
|
323
|
+
resp = requests.post(upload_url, headers=headers, files=files, data=data)
|
|
324
|
+
resp.raise_for_status()
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
"status": "success",
|
|
328
|
+
"location_type": "appwrite_new_file",
|
|
329
|
+
"file_id": new_file_id,
|
|
330
|
+
"file_name": new_file_name,
|
|
331
|
+
"upload_response": resp.json(),
|
|
332
|
+
"download_url": f"{APPWRITE_ENDPOINT}/storage/buckets/{bucket_id}/files/{new_file_id}/view?project={APPWRITE_PROJECT_ID}"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
# remote (can't write back)
|
|
336
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp_out:
|
|
337
|
+
wb.save(tmp_out.name)
|
|
338
|
+
fallback = tmp_out.name
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
"status": "success",
|
|
342
|
+
"location_type": "remote_readonly",
|
|
343
|
+
"output_path": fallback,
|
|
344
|
+
"message": "無法寫回遠端,只能輸出本機暫存檔"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ==============================
|
|
349
|
+
# 🔧 MCP Tool
|
|
350
|
+
# ==============================
|
|
351
|
+
|
|
352
|
+
@app.tool()
|
|
353
|
+
async def process_excel(url: str):
|
|
354
|
+
return await _process_excel_logic(url)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ==============================
|
|
358
|
+
# 🚀 CLI Test
|
|
359
|
+
# ==============================
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
test_url = (
|
|
363
|
+
"https://sgp.cloud.appwrite.io/v1/storage/buckets/6904374b00056677a970/files/6937a7fb00180f83ab67/view?project=6901b22e0036150b66d3&mode=admin"
|
|
364
|
+
)
|
|
365
|
+
print("🚀 測試開始...")
|
|
366
|
+
result = asyncio.run(_process_excel_logic(test_url))
|
|
367
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
from openai import OpenAI
|
|
3
|
+
import requests
|
|
4
|
+
from openpyxl import load_workbook
|
|
5
|
+
import tempfile, os, json
|
|
6
|
+
|
|
7
|
+
# 初始化 MCP 伺服器
|
|
8
|
+
app = FastMCP("ExcelProcessor")
|
|
9
|
+
|
|
10
|
+
# 初始化 OpenAI
|
|
11
|
+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "sk-your-key-here"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _process_excel_logic(url: str):
|
|
15
|
+
"""
|
|
16
|
+
處理 Excel:讀取 itemA~D,送給 LLM,回傳更新後的 Excel。
|
|
17
|
+
"""
|
|
18
|
+
# 下載 Excel
|
|
19
|
+
#resp = requests.get(url)
|
|
20
|
+
#resp.raise_for_status()
|
|
21
|
+
|
|
22
|
+
#with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
|
|
23
|
+
# tmp.write(resp.content)
|
|
24
|
+
# tmp_path = tmp.name
|
|
25
|
+
|
|
26
|
+
# 支援 file:/// 或 http(s)
|
|
27
|
+
if url.startswith("file:///"):
|
|
28
|
+
file_path = url.replace("file:///", "")
|
|
29
|
+
elif url.startswith("http://") or url.startswith("https://"):
|
|
30
|
+
resp = requests.get(url)
|
|
31
|
+
resp.raise_for_status()
|
|
32
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
|
|
33
|
+
tmp.write(resp.content)
|
|
34
|
+
file_path = tmp.name
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"不支援的檔案來源: {url}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
wb = load_workbook(file_path)
|
|
40
|
+
ws = wb.active
|
|
41
|
+
|
|
42
|
+
# 找出欄位標頭
|
|
43
|
+
header = {cell.value: idx for idx, cell in enumerate(ws[1], start=1)}
|
|
44
|
+
required = ["itemA", "itemB", "itemC", "itemD", "Result", "Reference"]
|
|
45
|
+
for r in required:
|
|
46
|
+
if r not in header:
|
|
47
|
+
raise ValueError(f"缺少欄位: {r}")
|
|
48
|
+
|
|
49
|
+
for row in ws.iter_rows(min_row=2, values_only=False):
|
|
50
|
+
a = row[header["itemA"] - 1].value or ""
|
|
51
|
+
b = row[header["itemB"] - 1].value or ""
|
|
52
|
+
c = row[header["itemC"] - 1].value or ""
|
|
53
|
+
d = row[header["itemD"] - 1].value or ""
|
|
54
|
+
if not any([a, b, c, d]):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
prompt = f"""
|
|
58
|
+
你是一個品質檢驗AI,請根據以下項目輸出結果:
|
|
59
|
+
|
|
60
|
+
itemA: {a}
|
|
61
|
+
itemB: {b}
|
|
62
|
+
itemC: {c}
|
|
63
|
+
itemD: {d}
|
|
64
|
+
|
|
65
|
+
請輸出 JSON:
|
|
66
|
+
{{"Result": "Conform / Half Conform / Not Conform", "Reference": "說明依據"}}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
res = client.responses.create(
|
|
70
|
+
model="gpt-4o-mini",
|
|
71
|
+
input=prompt,
|
|
72
|
+
response_format={"type": "json_object"}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
result_json = json.loads(res.output[0].content[0].text)
|
|
76
|
+
|
|
77
|
+
ws.cell(row=row[0].row, column=header["Result"], value=result_json.get("Result"))
|
|
78
|
+
ws.cell(row=row[0].row, column=header["Reference"], value=result_json.get("Reference"))
|
|
79
|
+
|
|
80
|
+
# 輸出結果 Excel
|
|
81
|
+
out_path = os.path.join(tempfile.gettempdir(), f"updated_{os.path.basename(tmp_path)}")
|
|
82
|
+
wb.save(out_path)
|
|
83
|
+
|
|
84
|
+
# 不用 schema,直接回傳 JSON 結構
|
|
85
|
+
return {
|
|
86
|
+
"status": "success",
|
|
87
|
+
"output_path": out_path,
|
|
88
|
+
"message": "Excel 已更新完成"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# 對外註冊為 MCP 工具
|
|
93
|
+
@app.tool()
|
|
94
|
+
def process_excel(url: str):
|
|
95
|
+
return _process_excel_logic(url)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
#app.run()
|
|
100
|
+
|
|
101
|
+
# ✅ 這裡放你的測試 Excel 路徑(本地或網址都可以)
|
|
102
|
+
test_path = r"C:\Users\Evan\Downloads\test_excel.xlsx"
|
|
103
|
+
test_url = f"file:///{test_path}" # 轉成 file:// URL 格式
|
|
104
|
+
|
|
105
|
+
# 呼叫函式直接測試
|
|
106
|
+
print("🚀 開始測試 process_excel ...")
|
|
107
|
+
result = _process_excel_logic(test_url)
|
|
108
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unieai_mcp_accton_rfp
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: Internal MCP server for Accton RFP automation (UnieAI)
|
|
5
|
+
Home-page: https://www.unieai.com/
|
|
6
|
+
Author: UnieAI
|
|
7
|
+
Author-email: contact@unieai.com
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: fastmcp>=0.3.0
|
|
16
|
+
Requires-Dist: fastapi>=0.104.0
|
|
17
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
19
|
+
Requires-Dist: requests>=2.31.0
|
|
20
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
Requires-Dist: openai>=1.35.10
|
|
23
|
+
Requires-Dist: langchain>=0.3.7
|
|
24
|
+
Requires-Dist: langchain-core>=0.3.9
|
|
25
|
+
Requires-Dist: langchain-openai>=0.2.3
|
|
26
|
+
Requires-Dist: aiohttp>=3.9.5
|
|
27
|
+
Requires-Dist: tiktoken>=0.7.0
|
|
28
|
+
Requires-Dist: rich>=13.7.1
|
|
29
|
+
Requires-Dist: tenacity>=8.3.0
|
|
30
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
unieai_mcp_accton_rfp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
unieai_mcp_accton_rfp/server.py,sha256=Zm5oG45p0mNSCV1eEhMnXGXY0KPWAhTvxbPuTMiRUgk,12795
|
|
3
|
+
unieai_mcp_accton_rfp/server_old.py,sha256=8rgIsqIgPUE0TcUhpdTeY6S_tpZazfP_UBwiSZ9NWYw,6971
|
|
4
|
+
unieai_mcp_accton_rfp/server_v0.py,sha256=kLFUujGj7MYXj_dpNBgvG2jvwPwT5EaKKfTwbAF8ebo,11455
|
|
5
|
+
unieai_mcp_accton_rfp/server_v02.py,sha256=tOsAUWqmRYwE8OWBQ-2Q_LDWM5osbryGNrSPK1Oo63s,11525
|
|
6
|
+
unieai_mcp_accton_rfp/server_v1.py,sha256=VXl-ciOsRDxBJ0mOGdjrZC8WHnihkZF2-lXcL4_ZIM4,5412
|
|
7
|
+
unieai_mcp_accton_rfp/server_v2.py,sha256=AwaxI4pJ7gEKI28TaLCUzImxTtmzxGdglT4i5unhWIY,8454
|
|
8
|
+
unieai_mcp_accton_rfp/server_v3.py,sha256=hBInfPC2WyGLH1Ibe15Rc6GHX8VQc8qJNyJ1vYuk__k,6343
|
|
9
|
+
unieai_mcp_accton_rfp/server_v4.py,sha256=VcWiH0D6bgj-mUkChe4w8ErJ-TXRA-1oNF6yPNcCg8I,12736
|
|
10
|
+
unieai_mcp_accton_rfp/server_v6.py,sha256=s-hZFjv1Qz4zL4d95Pyn3JhDgr0p2wcQY7-MtpS_gi0,11380
|
|
11
|
+
unieai_mcp_accton_rfp/server_v7.py,sha256=5hYeY3uUn4ag-2MrTae-srX7jp3w83-6PPYbVG_3tvc,11834
|
|
12
|
+
unieai_mcp_accton_rfp/test.py,sha256=VqUmvAFmmSxXAknTen6LCwXq5xH4jnltVT695ud-EMw,3482
|
|
13
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/licenses/LICENSE,sha256=u7gkpTabHIU9lqIRmF8e4LOLW60eioHSRkqrRnHdSL0,557
|
|
14
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/METADATA,sha256=pxyJpxAz24R3u0IT96Wd6khwVfcVSSvEvJbbzYvkWf0,995
|
|
15
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/entry_points.txt,sha256=NSVfAN8kCU9ITsA0umqWUIT7KdZ_iGpjJeQqwzaehV4,76
|
|
17
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/top_level.txt,sha256=XjtVqD2Dzddc66NJfoYsuNOLMUISFKru3qG97g-xLJI,22
|
|
18
|
+
unieai_mcp_accton_rfp-0.0.11.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Internal Use License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 UnieAI
|
|
4
|
+
|
|
5
|
+
This software is proprietary and confidential. It is intended solely for internal use within UnieAI. No part of this software may be copied, modified, distributed, or used outside UnieAI without prior written consent.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL UNIEAI BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
unieai_mcp_accton_rfp
|