File size: 9,994 Bytes
7c82621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f63c27a
7c82621
 
 
4d32f96
7c82621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d32f96
 
 
 
 
 
 
7c82621
 
4d32f96
7c82621
4d32f96
 
 
 
7c82621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5afd13
 
 
 
7c82621
e5afd13
7c82621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d24c184
 
 
 
 
 
 
 
 
 
 
 
 
7c82621
d24c184
 
 
 
be04c5f
d24c184
be04c5f
d24c184
 
be04c5f
 
 
 
 
 
 
d24c184
 
be04c5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d24c184
be04c5f
 
d24c184
be04c5f
 
d24c184
be04c5f
7c82621
 
d24c184
 
 
 
 
7c82621
d24c184
7c82621
 
 
e5afd13
7c82621
 
edd4216
7c82621
 
e5afd13
 
 
7c82621
 
 
 
 
 
4d32f96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c82621
edd4216
 
 
7c82621
edd4216
 
7c82621
 
 
 
4d32f96
 
7c82621
 
 
 
4d32f96
7c82621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
"""
nodes.py — LangGraph 各節點實作
每個節點都是純函數:(state) → dict(partial state update)
"""
import os
import time
import uuid
import json
import httpx
import asyncio
from typing import Optional

# 服務 URL(由環境變數設定,在 HuggingFace Spaces 間通信)
CREW_SERVICE_URL = os.getenv("CREW_SERVICE_URL", "http://localhost:7861")

# 分類用的 LLM
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_groq import ChatGroq

def _get_classifier_llm():
    """用最快速的 LLM 做意圖分類"""
    groq_key = os.getenv("GROQ_KEY_1") or os.getenv("GROQ_KEY")
    if groq_key:
        return ChatGroq(model="llama-3.1-8b-instant", groq_api_key=groq_key, temperature=0)
    google_key = os.getenv("GOOGLE_API_KEY_1") or os.getenv("GOOGLE_API_KEY")
    if google_key:
        return ChatGoogleGenerativeAI(
            model="gemini-1.5-flash",
            google_api_key=google_key,
            temperature=0,
            convert_system_message_to_human=True,
        )
    raise RuntimeError("No LLM keys available for classifier")

CLASSIFIER_LLM = None

def _classifier():
    global CLASSIFIER_LLM
    if CLASSIFIER_LLM is None:
        CLASSIFIER_LLM = _get_classifier_llm()
    return CLASSIFIER_LLM


# ── 節點實作 ──────────────────────────────────────────────────────────────────

def classify_intent(state: dict) -> dict:
    """
    節點 1:分析用戶輸入,決定意圖和部門
    """
    user_input = state["user_input"]
    prompt = f"""
你是 AI 公司的任務分配系統。分析以下用戶輸入,回傳 JSON。

用戶輸入:{user_input}

可用意圖(按優先級判斷):
- code: 需要【寫程式碼、開發、建立、實作】任何軟體、網站、應用、腳本、工具、API、系統。
  例如:寫網站、建立 API、開發 App、寫腳本、實作功能、建資料庫
- review: 需要 code review、審查程式碼、檢查 bug、技術審查
- strategy: 需要制定【商業策略、OKR、市場計劃】,純規劃不涉及實作
- prd: 需要撰寫產品需求文件、功能規格書
- marketing: 需要行銷文案、社群貼文、部落格、郵件
- research: 需要市場研究、競品分析、行業報告
- sales: 需要銷售文案、冷郵件、客戶開發
- chat: 一般問答

判斷規則:
- 只要提到「寫」、「建」、「開發」、「實作」、「製作」加上任何軟體/網站/工具,一律是 code
- 「網站」、「App」、「API」、「腳本」相關需求,一律是 code
- 只有純商業規劃、不涉及任何實作,才是 strategy

只回傳 JSON,不要其他文字:
{{"intent": "...", "department": "...", "summary": "30 字內的任務摘要"}}
"""
    try:
        result = _classifier().invoke(prompt)
        content = result.content if hasattr(result, "content") else str(result)
        # 清理可能的 markdown code block
        content = content.strip().strip("```json").strip("```").strip()
        parsed = json.loads(content)
        return {
            "intent": parsed.get("intent", "chat"),
            "department": parsed.get("department", "management"),
            "messages": [{"role": "system", "content": f"Intent classified: {parsed}"}],
        }
    except Exception as e:
        return {
            "intent": "chat",
            "department": "management",
            "error": f"Classification failed: {e}",
        }


def route_to_department(state: dict) -> dict:
    """節點 2:確認路由(可加業務邏輯)"""
    return {"job_id": str(uuid.uuid4())[:8], "retry_count": 0}


def _post_to_crew(endpoint: str, payload: dict, job_id: str) -> dict:
    try:
        resp = httpx.post(
            f"{CREW_SERVICE_URL}/{endpoint}",
            json={**payload, "job_id": job_id},
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        returned_job_id = data.get("job_id", job_id)
        print(f"[crew] submitted to /{endpoint}, job_id: {returned_job_id}")
        return {"job_status": "queued", "job_id": returned_job_id}
    except Exception as e:
        print(f"[crew] error posting to /{endpoint}: {e}")
        return {"error": str(e), "job_status": "error"}


def call_crew_strategy(state: dict) -> dict:
    """節點:呼叫 CrewAI CEO 制定策略"""
    return _post_to_crew(
        "strategy",
        {"objective": state["user_input"]},
        state["job_id"],
    )


def call_crew_prd(state: dict) -> dict:
    """節點:呼叫 CrewAI PM 撰寫 PRD"""
    return _post_to_crew(
        "product-spec",
        {"feature_request": state["user_input"]},
        state["job_id"],
    )


def call_crew_marketing(state: dict) -> dict:
    """節點:呼叫 CrewAI 市場部"""
    # 試著從輸入推斷 content_type
    user_input = state["user_input"].lower()
    content_type = "blog"
    for ct in ["twitter", "linkedin", "email", "newsletter", "blog"]:
        if ct in user_input:
            content_type = ct
            break
    return _post_to_crew(
        "marketing",
        {"topic": state["user_input"], "content_type": content_type},
        state["job_id"],
    )


def call_crew_research(state: dict) -> dict:
    """節點:呼叫 CrewAI 分析師做市場研究"""
    return _post_to_crew(
        "research",
        {"query": state["user_input"]},
        state["job_id"],
    )


def call_code_generation(state: dict) -> dict:
    """直接用 Groq 生成程式碼,不依賴 OpenHands"""
    from langchain_groq import ChatGroq

    groq_key = os.getenv("GROQ_KEY_1") or os.getenv("GROQ_KEY_2")
    if not groq_key:
        return {"job_status": "error", "error": "No Groq key available"}

    llm = ChatGroq(
        model="llama-3.3-70b-versatile",
        groq_api_key=groq_key,
        temperature=0,
    )

    intent = state.get("intent", "code")
    user_input = state["user_input"]

    if intent == "review":
        prompt = f"""You are a senior software engineer. Review the following code.

Code:
{user_input}

Output in English only:
1. Issue list (severity: High/Medium/Low)
2. Improvement suggestions
3. Fixed complete code
4. Security considerations

Use Markdown format."""

    else:
        prompt = f"""You are a senior software engineer. Complete the following task.

Task: {user_input}

STRICT RULES:
1. ALL code comments, variable names, function names, and output text must be in ENGLISH only. No Chinese characters anywhere in the code.
2. ALL content inside the code (text, labels, placeholders, error messages) must be in ENGLISH.
3. Use ONLY standard library or the most common packages. Do NOT use any framework that requires separate installation (no Astro, no Next.js, no Nuxt, no SvelteKit) unless the user explicitly requested it by name.
4. If building a website, use plain HTML + CSS + JavaScript in a single file, or use a minimal setup (e.g. Vite + vanilla JS). Output must be immediately runnable with minimal setup.
5. If a framework is absolutely necessary, provide the COMPLETE setup commands and explain every step.

Output format:
## Setup
(exact commands to run, copy-paste ready)

## File Structure
(list of files to create)

## Code
(complete code for each file, with English comments)

## How to Run
(exact run command)

Use Markdown format. Output in English only."""

    try:
        result = llm.invoke(prompt)
        return {
            "job_status": "done",
            "openhands_result": result.content,
        }
    except Exception as e:
        return {"job_status": "error", "error": str(e)}


def wait_for_job(state: dict) -> dict:
    job_id = state.get("job_id")
    retry = state.get("retry_count", 0)

    MAX_RETRIES = 100
    POLL_INTERVAL = 5

    if not job_id or job_id == "None":
        return {"job_status": "error", "error": "job_id is None, task was not submitted correctly"}

    if retry >= MAX_RETRIES:
        return {"job_status": "error", "error": "Timeout: job took too long"}

    time.sleep(POLL_INTERVAL)

    try:
        resp = httpx.get(
            f"{CREW_SERVICE_URL}/jobs/{job_id}",
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        status = data.get("status", "running")
        print(f"[wait_job] CrewAI job {job_id} status: {status}")

        if status == "done":
            return {"job_status": "done", "crew_result": data.get("result", "")}
        elif status == "error":
            return {
                "job_status": "error",
                "error": data.get("error", "CrewAI unknown error"),
            }
        return {"job_status": "running", "retry_count": retry + 1}

    except httpx.HTTPStatusError as e:
        print(f"[wait_job] HTTP error: {e.response.status_code} - {e.response.text}")
        return {"job_status": "running", "retry_count": retry + 1}
    except Exception as e:
        print(f"[wait_job] Exception: {type(e).__name__}: {e}")
        return {"job_status": "running", "retry_count": retry + 1}

def format_response(state: dict) -> dict:
    crew_result = state.get("crew_result") or ""
    openhands_result = state.get("openhands_result") or ""
    
    if crew_result:
        final = crew_result
    elif openhands_result:
        final = openhands_result
    else:
        final = "任務已完成,但沒有收到回應內容。"

    return {
        "final_response": final,
        "messages": [{"role": "assistant", "content": final}],
    }


def handle_error(state: dict) -> dict:
    """節點:錯誤處理"""
    error = state.get("error", "未知錯誤")
    response = (
        f"⚠️ 任務執行遇到問題:\n\n```\n{error}\n```\n\n"
        "請稍後重試,或簡化您的請求。"
    )
    return {
        "final_response": response,
        "messages": [{"role": "assistant", "content": response}],
    }