File size: 6,562 Bytes
a547d6c
116524e
1cf3e52
 
116524e
 
d94bec9
a547d6c
1cf3e52
116524e
1cf3e52
116524e
 
 
 
 
 
 
 
 
1cf3e52
 
 
 
 
 
 
 
116524e
 
c2c9236
 
 
 
 
 
 
 
 
 
 
1cf3e52
 
c2c9236
116524e
1cf3e52
 
 
 
 
fb91a77
116524e
 
c2c9236
 
 
 
 
a547d6c
 
 
 
1cf3e52
c2c9236
a547d6c
 
 
 
c2c9236
a547d6c
6118c36
 
 
 
 
 
 
 
 
 
 
c2c9236
 
a547d6c
 
6118c36
a547d6c
c2c9236
a547d6c
c2c9236
 
 
 
a547d6c
 
 
 
d94bec9
6118c36
d94bec9
 
 
 
 
 
 
 
 
 
6118c36
d94bec9
 
 
 
 
 
 
 
 
 
6118c36
d94bec9
6118c36
 
 
 
 
 
 
 
1cf3e52
 
 
 
 
 
 
 
 
a547d6c
 
6118c36
 
c2c9236
aa9a63b
1cf3e52
 
 
 
 
 
aa9a63b
 
 
 
 
 
 
1cf3e52
 
aa9a63b
 
 
1cf3e52
aa9a63b
116524e
 
 
1cf3e52
 
 
 
 
 
 
 
 
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
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import os
import json
import httpx
import redis

app = FastAPI(title="Manus Clone")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── Redis connection (2GB, localhost) ──────────────────────────────────────────
try:
    r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
    r.ping()
    REDIS_OK = True
except Exception:
    REDIS_OK = False

class ChatRequest(BaseModel):
    prompt: str
    model: str = "llama-3.1-8b-instant"
    api_key: str = ""
    provider: str = "Groq"

PROVIDER_BASES = {
    "OpenAI":      "https://api.openai.com/v1",
    "Anthropic":   "https://api.anthropic.com/v1",
    "Groq":        "https://api.groq.com/openai/v1",
    "HuggingFace": "https://api-inference.huggingface.co/v1",
    "OpenRouter":  "https://openrouter.ai/api/v1",
    "Nvidia":      "https://integrate.api.nvidia.com/v1",
    "Mistral":     "https://api.mistral.ai/v1",
    "Cohere":      "https://api.cohere.ai/v1",
}

# ── API endpoints ─────────────────────────────────────────────────────────────

@app.get("/health")
def health():
    return {"status": "ok", "redis": REDIS_OK}

@app.post("/chat")
async def chat_endpoint(request: ChatRequest):
    api_key = (
        request.api_key.strip()
        or os.environ.get("GROQ_API_KEY", "")
        or os.environ.get("OPENAI_API_KEY", "")
    )

    if not api_key:
        return {
            "response": (
                f'[Manus] Received: "{request.prompt}"\n\n'
                "No API key configured. Please add your key via the Providers panel."
            ),
            "doc": True
        }

    base_url = PROVIDER_BASES.get(request.provider, PROVIDER_BASES["Groq"])

    # Build headers
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }

    # OpenRouter needs extra headers for some models
    if request.provider == "OpenRouter":
        headers["HTTP-Referer"] = "https://augment17-logic-engine.hf.space"
        headers["X-Title"] = "Manus Clone"

    try:
        async with httpx.AsyncClient(timeout=60) as client:
            resp = await client.post(
                f"{base_url}/chat/completions",
                headers=headers,
                json={
                    "model": request.model,
                    "messages": [
                        {"role": "system", "content": "You are Manus, a helpful and concise autonomous AI agent."},
                        {"role": "user", "content": request.prompt},
                    ],
                    "temperature": 0.7,
                }
            )
            resp.raise_for_status()
            data = resp.json()

            # Robust response extraction
            reply = None
            try:
                choices = data.get("choices", [])
                if choices and len(choices) > 0:
                    choice = choices[0]
                    msg = choice.get("message") or choice.get("delta") or {}
                    reply = msg.get("content") or choice.get("text") or ""
            except (KeyError, IndexError, TypeError):
                pass

            # Fallback keys
            if not reply:
                reply = (
                    data.get("text")
                    or data.get("output")
                    or data.get("response")
                    or data.get("content")
                    or data.get("result")
                    or ""
                )

            # Empty choices = model not found / not available
            if not reply:
                raise HTTPException(
                    status_code=404,
                    detail=(
                        f"Model '{request.model}' returned an empty response from {request.provider}. "
                        f"This usually means the model does not exist on {request.provider}'s API. "
                        f"Try a different model or switch to OpenRouter which supports more models."
                    )
                )

            # Cache to Redis if available
            if REDIS_OK:
                try:
                    cache_key = f"chat:{request.provider}:{request.model}:{request.prompt[:100]}"
                    r.setex(cache_key, 3600, reply)
                except Exception:
                    pass

            return {"response": reply, "doc": True}

    except HTTPException:
        raise
    except httpx.HTTPStatusError as e:
        status = e.response.status_code
        try:
            err_body = e.response.json()
            err_msg = err_body.get("error", {}).get("message", e.response.text[:300])
        except Exception:
            err_msg = e.response.text[:300]

        if status == 401:
            detail = (
                f"401 Unauthorized from {request.provider}. "
                f"Check that your API key is valid for {request.provider} "
                f"and that the model '{request.model}' is available on {request.provider}. "
                f"Tip: Make sure the model you selected belongs to the same provider as your API key."
            )
        elif status == 400:
            detail = f"400 Bad Request from {request.provider}: {err_msg}"
        elif status == 404:
            detail = f"404: Model '{request.model}' not found on {request.provider}. Select a model from the correct provider tab."
        else:
            detail = f"Provider error {status}: {err_msg}"
        raise HTTPException(status_code=status, detail=detail)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# ── Serve React UI (MUST be last, catches all non-API routes) ─────────────────
static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.isdir(static_dir):
    @app.get("/{full_path:path}")
    async def serve_spa(full_path: str):
        file_path = os.path.join(static_dir, full_path)
        if full_path and os.path.isfile(file_path):
            return FileResponse(file_path)
        return FileResponse(os.path.join(static_dir, "index.html"))