franksoo commited on
Commit
4a78bc3
ยท
1 Parent(s): 191f7d7
Files changed (3) hide show
  1. Dockerfile +1 -1
  2. app.py +158 -33
  3. requirements.txt +4 -3
Dockerfile CHANGED
@@ -4,4 +4,4 @@ COPY requirements.txt .
4
  RUN pip install --no-cache-dir -r requirements.txt
5
  COPY . .
6
  EXPOSE 7860
7
- CMD ["python", "app.py"]
 
4
  RUN pip install --no-cache-dir -r requirements.txt
5
  COPY . .
6
  EXPOSE 7860
7
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,52 +1,177 @@
1
  import os
2
  import hashlib
3
  import base64
4
- import urllib.parse
 
 
 
5
  from fastapi import FastAPI, Request
6
- from fastapi.responses import PlainTextResponse
 
7
  from Crypto.Cipher import AES
8
  from Crypto.Util.Padding import unpad
 
9
 
10
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # ไปŽ็Žฏๅขƒๅ˜้‡่ฏปๅ–๏ผˆๅฟ…้กปๅ’Œไผไธšๅพฎไฟกไธ€่‡ด๏ผ‰
13
- WECOM_TOKEN = os.getenv("WECOM_TOKEN", "").strip()
14
- WECOM_AES_KEY = os.getenv("WECOM_AES_KEY", "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- # ==========================================
17
- # ไผไธšๅพฎไฟกๅฎ˜ๆ–น URL ้ชŒ่ฏ๏ผˆ็ปๅฏนๆญฃ็กฎ๏ผ‰
18
- # ==========================================
19
  @app.get("/gateway/wecom")
20
- async def verify(request: Request):
 
21
  try:
22
- # ่Žทๅ–ๅ‚ๆ•ฐ
23
- msg_signature = request.query_params.get("msg_signature", "")
24
  timestamp = request.query_params.get("timestamp", "")
25
- nonce = request.query_params.get("nonce", "")
26
- echostr_enc = request.query_params.get("echostr", "")
27
-
28
- # ้ชŒ็ญพ
29
- arr = sorted([WECOM_TOKEN, timestamp, nonce])
30
- check_str = ''.join(arr)
31
- sig = hashlib.sha1(check_str.encode()).hexdigest()
32
 
33
- if sig != msg_signature:
 
34
  return PlainTextResponse("")
35
 
36
- # AES ่งฃๅฏ†๏ผˆๅฎ˜ๆ–นๆ ‡ๅ‡†๏ผ‰
37
- aes_key = base64.b64decode(WECOM_AES_KEY)
38
- cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
39
- encrypted = base64.b64decode(echostr_enc)
40
- decrypted = unpad(cipher.decrypt(encrypted), AES.block_size)
41
- msg = decrypted[16:].decode()
42
-
43
- # ๅช่ฟ”ๅ›žๆ˜Žๆ–‡๏ผ๏ผ๏ผ ไธๅŠ ไปปไฝ•ไธœ่ฅฟ๏ผ๏ผ๏ผ
44
- return PlainTextResponse(msg)
45
-
46
  except Exception:
47
  return PlainTextResponse("")
48
 
49
- # ๅฟ…้กป่ฟ”ๅ›ž็ฉบ success
50
  @app.post("/gateway/wecom")
51
- async def post():
52
- return PlainTextResponse("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import hashlib
3
  import base64
4
+ import struct
5
+ import threading
6
+ import httpx
7
+ import gradio as gr
8
  from fastapi import FastAPI, Request
9
+ from fastapi.responses import PlainTextResponse, HTMLResponse
10
+ from openai import OpenAI
11
  from Crypto.Cipher import AES
12
  from Crypto.Util.Padding import unpad
13
+ import xml.etree.ElementTree as ET
14
 
15
+ # โ”€โ”€ ็Žฏๅขƒๅ˜้‡ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
+ NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
17
+ WECOM_TOKEN = os.getenv("WECOM_TOKEN", "").strip()
18
+ WECOM_AES_KEY = os.getenv("WECOM_AES_KEY", "").strip()
19
+ WECOM_CORPID = os.getenv("CORPID", "").strip()
20
+ WECOM_SECRET = os.getenv("WECOM_SECRET", "").strip()
21
+ WECOM_AGENTID = os.getenv("WECOM_AGENTID", "1000003")
22
+
23
+ # โ”€โ”€ Hermes LLM ๅฎขๆˆท็ซฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
24
+ llm = OpenAI(
25
+ base_url="https://integrate.api.nvidia.com/v1",
26
+ api_key=NVIDIA_API_KEY,
27
+ )
28
+
29
+ SYSTEM_PROMPT = (
30
+ "You are Hermes, a helpful AI assistant. "
31
+ "Answer concisely and accurately. Reply in the same language as the user."
32
+ )
33
+
34
+ def chat_with_hermes(message: str, history: list) -> str:
35
+ """่ฐƒ็”จ Hermes ๆจกๅž‹๏ผŒ่ฟ”ๅ›žๅ›žๅคๆ–‡ๆœฌ"""
36
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
37
+ for user_msg, bot_msg in history:
38
+ messages.append({"role": "user", "content": user_msg})
39
+ messages.append({"role": "assistant", "content": bot_msg})
40
+ messages.append({"role": "user", "content": message})
41
+
42
+ response = llm.chat.completions.create(
43
+ model="nv-mistralai/mistral-nemo-12b-instruct",
44
+ messages=messages,
45
+ max_tokens=1024,
46
+ temperature=0.7,
47
+ )
48
+ return response.choices[0].message.content
49
+
50
+ # โ”€โ”€ ไผไธšๅพฎไฟก Access Token โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
51
+ _wx_token_cache = {"token": "", "expires": 0}
52
+ _token_lock = threading.Lock()
53
+
54
+ def get_wecom_token() -> str:
55
+ import time
56
+ with _token_lock:
57
+ if time.time() < _wx_token_cache["expires"] - 60:
58
+ return _wx_token_cache["token"]
59
+ url = (
60
+ f"https://qyapi.weixin.qq.com/cgi-bin/gettoken"
61
+ f"?corpid={WECOM_CORPID}&corpsecret={WECOM_SECRET}"
62
+ )
63
+ r = httpx.get(url, timeout=10)
64
+ data = r.json()
65
+ _wx_token_cache["token"] = data.get("access_token", "")
66
+ _wx_token_cache["expires"] = time.time() + data.get("expires_in", 7200)
67
+ return _wx_token_cache["token"]
68
 
69
+ def send_wecom_message(to_user: str, content: str):
70
+ """ๅ‘ไผไธšๅพฎไฟก็”จๆˆทๅ‘้€ๆ–‡ๆœฌๆถˆๆฏ"""
71
+ token = get_wecom_token()
72
+ url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}"
73
+ payload = {
74
+ "touser": to_user,
75
+ "msgtype": "text",
76
+ "agentid": int(WECOM_AGENTID),
77
+ "text": {"content": content},
78
+ }
79
+ httpx.post(url, json=payload, timeout=10)
80
+
81
+ # โ”€โ”€ ไผไธšๅพฎไฟกๆถˆๆฏ่งฃๅฏ† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
82
+ def decrypt_wecom_msg(encrypted: str) -> str:
83
+ aes_key = base64.b64decode(WECOM_AES_KEY + "=") # ่กฅ้ฝ padding
84
+ cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
85
+ raw = unpad(cipher.decrypt(base64.b64decode(encrypted)), AES.block_size)
86
+ # ๆ ผๅผ: 16ๅญ—่Š‚้šๆœบ + 4ๅญ—่Š‚้•ฟๅบฆ + ๅ†…ๅฎน + appid
87
+ msg_len = struct.unpack(">I", raw[16:20])[0]
88
+ return raw[20: 20 + msg_len].decode("utf-8")
89
+
90
+ def verify_signature(token: str, timestamp: str, nonce: str, encrypt: str = "") -> str:
91
+ arr = sorted([token, timestamp, nonce, encrypt] if encrypt else [token, timestamp, nonce])
92
+ return hashlib.sha1("".join(arr).encode()).hexdigest()
93
+
94
+ # โ”€โ”€ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
+ app = FastAPI()
96
 
 
 
 
97
  @app.get("/gateway/wecom")
98
+ async def wecom_verify(request: Request):
99
+ """ไผไธšๅพฎไฟก URL ้ชŒ่ฏ"""
100
  try:
101
+ msg_sig = request.query_params.get("msg_signature", "")
 
102
  timestamp = request.query_params.get("timestamp", "")
103
+ nonce = request.query_params.get("nonce", "")
104
+ echostr = request.query_params.get("echostr", "")
 
 
 
 
 
105
 
106
+ sig = verify_signature(WECOM_TOKEN, timestamp, nonce, echostr)
107
+ if sig != msg_sig:
108
  return PlainTextResponse("")
109
 
110
+ plain = decrypt_wecom_msg(echostr)
111
+ # ๅชๅ–ๆถˆๆฏๅ†…ๅฎน้ƒจๅˆ†๏ผˆๅŽปๆމๆœซๅฐพ appid๏ผ‰
112
+ return PlainTextResponse(plain)
 
 
 
 
 
 
 
113
  except Exception:
114
  return PlainTextResponse("")
115
 
 
116
  @app.post("/gateway/wecom")
117
+ async def wecom_message(request: Request):
118
+ """ๆŽฅๆ”ถไผไธšๅพฎไฟกๆถˆๆฏ๏ผŒ่ฐƒ็”จ Hermes ๅ›žๅค"""
119
+ try:
120
+ msg_sig = request.query_params.get("msg_signature", "")
121
+ timestamp = request.query_params.get("timestamp", "")
122
+ nonce = request.query_params.get("nonce", "")
123
+
124
+ body = await request.body()
125
+ xml_root = ET.fromstring(body.decode("utf-8"))
126
+ encrypt = xml_root.findtext("Encrypt", "")
127
+
128
+ sig = verify_signature(WECOM_TOKEN, timestamp, nonce, encrypt)
129
+ if sig != msg_sig:
130
+ return PlainTextResponse("success")
131
+
132
+ plain_xml = decrypt_wecom_msg(encrypt)
133
+ msg_root = ET.fromstring(plain_xml)
134
+ msg_type = msg_root.findtext("MsgType", "")
135
+ from_user = msg_root.findtext("FromUserName", "")
136
+
137
+ if msg_type == "text":
138
+ user_content = msg_root.findtext("Content", "").strip()
139
+ if user_content:
140
+ # ๅผ‚ๆญฅๅ›žๅค๏ผŒ้ฟๅ…่ถ…ๆ—ถ
141
+ def reply():
142
+ answer = chat_with_hermes(user_content, [])
143
+ send_wecom_message(from_user, answer)
144
+ threading.Thread(target=reply, daemon=True).start()
145
+
146
+ except Exception:
147
+ pass
148
+ return PlainTextResponse("success")
149
+
150
+ # โ”€โ”€ Gradio Web ็•Œ้ข โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
151
+ def gradio_chat(message: str, history: list):
152
+ reply = chat_with_hermes(message, history)
153
+ history.append((message, reply))
154
+ return "", history
155
+
156
+ with gr.Blocks(title="Hermes Agent", theme=gr.themes.Soft()) as demo:
157
+ gr.Markdown("# ๐Ÿค– Hermes Agent\nPowered by NVIDIA NIM ยท ไผไธšๅพฎไฟกๅŒๆญฅๆŽฅๅ…ฅ")
158
+ chatbot = gr.Chatbot(height=500, label="ๅฏน่ฏ")
159
+ with gr.Row():
160
+ msg = gr.Textbox(
161
+ placeholder="่พ“ๅ…ฅๆถˆๆฏ๏ผŒๆŒ‰ Enter ๅ‘้€...",
162
+ show_label=False,
163
+ scale=9,
164
+ )
165
+ clear = gr.Button("ๆธ…็ฉบ", scale=1)
166
+
167
+ msg.submit(gradio_chat, [msg, chatbot], [msg, chatbot])
168
+ clear.click(lambda: ([], ""), outputs=[chatbot, msg])
169
+
170
+ # โ”€โ”€ ๆŒ‚่ฝฝ Gradio ๅˆฐ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
171
+ import gradio.routes
172
+ app = gr.mount_gradio_app(app, demo, path="/")
173
+
174
+ # โ”€โ”€ ๅฏๅŠจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
175
+ if __name__ == "__main__":
176
+ import uvicorn
177
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  fastapi
2
- uvicorn
3
- gradio
4
  pycryptodome
5
- cryptography
 
 
1
  fastapi
2
+ uvicorn[standard]
3
+ gradio>=4.0.0
4
  pycryptodome
5
+ httpx
6
+ openai>=1.0.0