File size: 7,703 Bytes
a2aa01b
0d26c71
0129d33
4a78bc3
 
 
d87ef2f
4a78bc3
09c1d64
 
4a78bc3
0129d33
4a78bc3
09c1d64
a2aa01b
09c1d64
d87ef2f
 
 
 
 
 
 
4a78bc3
583db40
 
 
 
 
32b26bd
583db40
09c1d64
 
4a78bc3
 
 
 
 
 
 
d87ef2f
4a78bc3
7756287
583db40
123e831
 
 
 
c13bb7a
583db40
766cf9e
 
4a78bc3
 
 
 
d87ef2f
 
 
4a78bc3
09c1d64
583db40
 
4a78bc3
 
 
583db40
 
 
 
 
 
 
 
09c1d64
583db40
 
 
a2aa01b
4a78bc3
 
caed852
583db40
09c1d64
 
 
caed852
09c1d64
4a78bc3
583db40
815f964
 
4a78bc3
815f964
 
 
 
 
4a78bc3
 
 
583db40
 
 
4a78bc3
09c1d64
 
ae03e12
09c1d64
4a78bc3
caed852
09c1d64
0d26c71
09c1d64
 
 
caed852
687d112
09c1d64
 
caed852
 
 
687d112
93bd862
09c1d64
4a78bc3
caed852
 
4a78bc3
 
09c1d64
583db40
09c1d64
 
 
caed852
4a78bc3
583db40
caed852
 
 
 
583db40
09c1d64
583db40
4a78bc3
caed852
 
09c1d64
caed852
 
 
4a78bc3
caed852
 
4a78bc3
 
09c1d64
 
 
 
 
 
 
 
4a78bc3
 
32b26bd
09c1d64
32b26bd
09c1d64
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import os
import hashlib
import base64
import struct
import threading
import httpx
import logging
import gradio as gr
from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse
from openai import OpenAI
from Crypto.Cipher import AES
import xml.etree.ElementTree as ET
import uvicorn

# โ”€โ”€ ๆ—ฅๅฟ— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("hermes")

# โ”€โ”€ ็Žฏๅขƒๅ˜้‡ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "")
WECOM_TOKEN    = os.getenv("WECOM_TOKEN", "").strip()
WECOM_AES_KEY  = os.getenv("WECOM_AES_KEY", "").strip()
WECOM_CORPID   = os.getenv("CORPID", "").strip()
WECOM_SECRET   = os.getenv("WECOM_SECRET", "").strip()
WECOM_AGENTID  = os.getenv("WECOM_AGENTID", "1000003").strip() or "1000003"

# โ”€โ”€ LLM โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
llm = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NVIDIA_API_KEY)

SYSTEM_PROMPT = (
    "You are Hermes, a helpful AI assistant. "
    "Answer concisely and accurately. Reply in the same language as the user."
)

def chat_with_hermes(message: str, history: list) -> str:
    log.info("[WEB] user: %s", message)
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    for h in history:
        if isinstance(h, (list, tuple)) and len(h) == 2:
            if h[0]:
                messages.append({"role": "user", "content": str(h[0])})
            if h[1]:
                messages.append({"role": "assistant", "content": str(h[1])})
    messages.append({"role": "user", "content": message})
    resp = llm.chat.completions.create(
       # model="meta/llama-3.1-8b-instruct",
        model="minimaxai/minimax-m2.7",
        messages=messages,
        max_tokens=1024,
        temperature=0.7,
    )
    reply = resp.choices[0].message.content
    log.info("[WEB] assistant: %s", reply)
    return reply

# โ”€โ”€ ไผไธšๅพฎไฟก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_wx_cache = {"token": "", "expires": 0}
_lock = threading.Lock()

def get_wecom_token() -> str:
    import time
    with _lock:
        if time.time() < _wx_cache["expires"] - 60:
            return _wx_cache["token"]
        r = httpx.get(
            "https://qyapi.weixin.qq.com/cgi-bin/gettoken",
            params={"corpid": WECOM_CORPID, "corpsecret": WECOM_SECRET},
            timeout=10,
        ).json()
        log.info("[WECOM] get_token: %s", r)
        _wx_cache["token"] = r.get("access_token", "")
        _wx_cache["expires"] = time.time() + r.get("expires_in", 7200)
        return _wx_cache["token"]

def send_wecom_message(to_user: str, content: str):
    token = get_wecom_token()
    r = httpx.post(
        f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}",
        json={"touser": to_user, "msgtype": "text",
              "agentid": int(WECOM_AGENTID), "text": {"content": content}},
        timeout=10,
    ).json()
    log.info("[WECOM] send to=%s result=%s", to_user, r)

def decrypt_msg(encrypted: str) -> str:
    # ไผไธšๅพฎไฟก AESKey = Base64(43ไฝ) ่งฃ็ ๅŽๆฐๅฅฝ 32 ๅญ—่Š‚๏ผŒไธ้œ€่ฆ่กฅ =
    aes_key = base64.b64decode(WECOM_AES_KEY + "=")  # ่กฅไธ€ไธช = ๅ‡‘ๆˆ44ไฝๅˆๆณ•base64
    cipher = AES.new(aes_key, AES.MODE_CBC, aes_key[:16])
    raw = cipher.decrypt(base64.b64decode(encrypted))
    # ๆ‰‹ๅŠจๅŽป้™ค PKCS7 padding๏ผˆไผไธšๅพฎไฟกๆ ‡ๅ‡†๏ผ‰
    pad_len = raw[-1]
    raw = raw[:-pad_len]
    # ๆ ผๅผ: 16ๅญ—่Š‚้šๆœบ + 4ๅญ—่Š‚ๆถˆๆฏ้•ฟๅบฆ(big-endian) + ๆถˆๆฏๅ†…ๅฎน + corpid
    msg_len = struct.unpack(">I", raw[16:20])[0]
    return raw[20: 20 + msg_len].decode("utf-8")

def verify_sig(timestamp: str, nonce: str, encrypt: str = "") -> str:
    parts = [WECOM_TOKEN, timestamp, nonce] + ([encrypt] if encrypt else [])
    return hashlib.sha1("".join(sorted(parts)).encode()).hexdigest()

# โ”€โ”€ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
fastapi_app = FastAPI()

@fastapi_app.get("/gateway/wecom")
async def wecom_verify(request: Request):
    p = request.query_params
    log.info("[WECOM] GET params: %s", dict(p))
    try:
        sig = verify_sig(p.get("timestamp", ""), p.get("nonce", ""), p.get("echostr", ""))
        expected = p.get("msg_signature", "")
        log.info("[WECOM] sig=%s expected=%s match=%s", sig, expected, sig == expected)
        if sig != expected:
            return PlainTextResponse("")
        plain = decrypt_msg(p.get("echostr", ""))
        log.info("[WECOM] verify ok plain=%s", plain)
        return PlainTextResponse(plain)
    except Exception as e:
        log.exception("[WECOM] verify error: %s", e)
        return PlainTextResponse("")

@fastapi_app.post("/gateway/wecom")
async def wecom_message(request: Request):
    p = request.query_params
    log.info("[WECOM] POST params: %s", dict(p))
    try:
        body = await request.body()
        log.info("[WECOM] body: %s", body.decode())
        encrypt = ET.fromstring(body.decode()).findtext("Encrypt", "")
        sig = verify_sig(p.get("timestamp", ""), p.get("nonce", ""), encrypt)
        expected = p.get("msg_signature", "")
        log.info("[WECOM] sig=%s expected=%s match=%s", sig, expected, sig == expected)
        if sig != expected:
            return PlainTextResponse("success")
        root = ET.fromstring(decrypt_msg(encrypt))
        msg_type = root.findtext("MsgType")
        from_user = root.findtext("FromUserName", "")
        log.info("[WECOM] msg_type=%s from=%s", msg_type, from_user)
        if msg_type == "text":
            content = root.findtext("Content", "").strip()
            log.info("[WECOM] content=%s", content)
            if content:
                def reply():
                    try:
                        answer = chat_with_hermes(content, [])
                        log.info("[WECOM] reply=%s", answer)
                        send_wecom_message(from_user, answer)
                    except Exception as e:
                        log.exception("[WECOM] reply error: %s", e)
                threading.Thread(target=reply, daemon=True).start()
    except Exception as e:
        log.exception("[WECOM] message error: %s", e)
    return PlainTextResponse("success")

# โ”€โ”€ Gradio ๆŒ‚่ฝฝๅˆฐ FastAPI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
demo = gr.ChatInterface(
    fn=chat_with_hermes,
    title="๐Ÿค– Hermes Agent",
    description="Powered by NVIDIA NIM ยท ไผไธšๅพฎไฟกๅŒๆญฅๆŽฅๅ…ฅ",
)
app = gr.mount_gradio_app(fastapi_app, demo, path="/")

# โ”€โ”€ ๅฏๅŠจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if __name__ == "__main__":
    log.info("WECOM_TOKEN=%s AES_KEY=%s CORPID=%s SECRET=%s NVIDIA=%s AGENTID=%s",
             bool(WECOM_TOKEN), bool(WECOM_AES_KEY),
             bool(WECOM_CORPID), bool(WECOM_SECRET), bool(NVIDIA_API_KEY), WECOM_AGENTID)
    uvicorn.run(app, host="0.0.0.0", port=7860)