File size: 5,991 Bytes
178ff41
162e051
 
 
 
178ff41
162e051
3de8fcc
162e051
 
 
 
 
 
 
 
 
 
178ff41
 
 
 
d65751c
 
 
 
 
 
 
 
 
 
 
 
178ff41
 
3de8fcc
162e051
 
 
 
 
 
 
3de8fcc
 
162e051
 
 
 
 
 
 
 
 
3de8fcc
 
178ff41
162e051
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de8fcc
 
162e051
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de8fcc
 
162e051
178ff41
162e051
3de8fcc
 
162e051
 
 
 
 
 
 
 
 
 
 
178ff41
 
 
3de8fcc
162e051
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3de8fcc
 
162e051
3de8fcc
 
 
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
"""CoderInstruc Gradio Space — UI + agent API (remote HF inference, no local model)."""

from __future__ import annotations

import os
from typing import Any, Callable, TypeVar

import gradio as gr
import httpx

DEFAULT_MODEL = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
DEFAULT_ENDPOINT = "https://router.huggingface.co/v1"
SYSTEM_PROMPT = (
    "You are CoderInstruc, a coding assistant specialized in Python, TypeScript, Go, and SQL. "
    "Prefer correct, minimal, production-ready code. Use tools/schema style answers when asked "
    "to edit files or run tests. Refuse requests for malware or secret exfiltration."
)

F = TypeVar("F", bound=Callable[..., Any])


def gpu_if_zerogpu(fn: F) -> F:
    """
    ZeroGPU Spaces require at least one @spaces.GPU at startup.
    We only call remote Inference API — decorate when the real HF `spaces` lib is present.
    """
    try:
        import spaces  # type: ignore

        gpu = getattr(spaces, "GPU", None)
        if gpu is None:
            return fn
        return gpu(duration=120)(fn)  # type: ignore[return-value]
    except Exception:  # noqa: BLE001 — local / CPU / wrong PyPI package
        return fn


def _settings() -> dict[str, str]:
    token = os.getenv("HF_TOKEN") or os.getenv("CODERINSTRUC_HF_TOKEN") or ""
    model = os.getenv("CODERINSTRUC_HF_INFERENCE_MODEL") or DEFAULT_MODEL
    endpoint = (
        os.getenv("CODERINSTRUC_HF_INFERENCE_ENDPOINT") or DEFAULT_ENDPOINT
    ).rstrip("/")
    return {"token": token, "model": model, "endpoint": endpoint}


def _is_unsafe(text: str) -> bool:
    lowered = text.lower()
    blocked = (
        "ignore previous instructions",
        "bypass safety",
        "write ransomware",
        "create malware to steal",
    )
    return any(b in lowered for b in blocked)


@gpu_if_zerogpu
def complete(message: str, history: list[dict[str, str]] | None = None) -> str:
    """Core completion used by UI and agent API."""
    cfg = _settings()
    if not message.strip():
        return "Envie uma pergunta de código."
    if _is_unsafe(message):
        return "Request blocked by safety filter."
    if not cfg["token"]:
        return (
            "Configure o secret `HF_TOKEN` no Space "
            "(Settings → Variables and secrets) para habilitar a inferência."
        )

    messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
    for turn in history or []:
        role = turn.get("role")
        content = turn.get("content")
        if role in {"user", "assistant"} and content:
            messages.append({"role": role, "content": content})
    messages.append({"role": "user", "content": message})

    payload = {
        "model": cfg["model"],
        "messages": messages,
        "max_tokens": 2048,
        "temperature": 0.7,
        "top_p": 0.8,
        "stream": False,
    }
    headers = {
        "Authorization": f"Bearer {cfg['token']}",
        "Content-Type": "application/json",
    }
    url = f"{cfg['endpoint']}/chat/completions"
    try:
        with httpx.Client(timeout=180.0) as client:
            resp = client.post(url, json=payload, headers=headers)
            if resp.status_code >= 400:
                return f"Erro HF ({resp.status_code}): {resp.text[:600]}"
            data = resp.json()
        return data["choices"][0]["message"]["content"]
    except Exception as exc:  # noqa: BLE001
        return f"Falha ao chamar inference: {exc}"


def ask(prompt: str) -> str:
    """Stateless agent endpoint — api_name='ask' for /agents.md clients."""
    return complete(prompt, history=None)


def build_ui() -> gr.Blocks:
    cfg = _settings()
    with gr.Blocks(title="CoderInstruc") as demo:
        gr.Markdown(
            f"""
# CoderInstruc
Assistente de código (base Qwen3-Coder → post-train CoderInstruc).

**Modelo:** `{cfg["model"]}` · Inferência remota (pesos fora deste Space)

**Agents:** `curl https://huggingface.co/spaces/amarorn/CoderInstruc/agents.md`

> Preferência de hardware: **CPU Basic** (não precisa ZeroGPU). Se estiver em ZeroGPU, o app
> usa `@spaces.GPU` só para satisfazer o runtime — a carga real é a Inference API.
"""
        )
        with gr.Tab("Chat"):
            chatbot = gr.Chatbot(height=480)
            msg = gr.Textbox(
                placeholder="Ex.: Write a Python binary search with tests",
                label="Mensagem",
                lines=3,
            )
            with gr.Row():
                send = gr.Button("Enviar", variant="primary")
                clear = gr.Button("Limpar")

            def respond(user_msg: str, history: list[dict[str, str]] | None):
                reply = complete(user_msg, history)
                history = list(history or [])
                history.append({"role": "user", "content": user_msg})
                history.append({"role": "assistant", "content": reply})
                return history, ""

            send.click(respond, inputs=[msg, chatbot], outputs=[chatbot, msg])
            msg.submit(respond, inputs=[msg, chatbot], outputs=[chatbot, msg])
            clear.click(lambda: ([], ""), outputs=[chatbot, msg])

            gr.Examples(
                examples=[
                    "Write a Python function that reverses a string",
                    "TypeScript debounce utility with generics",
                    "SQL: top 5 users by order count",
                ],
                inputs=msg,
            )

        with gr.Tab("Agent API"):
            gr.Markdown(
                "Endpoint estático para agents (`api_name=ask`). "
                "Também disponível via Gradio API / `agents.md`."
            )
            prompt = gr.Textbox(label="prompt", lines=4)
            answer = gr.Textbox(label="answer", lines=12)
            ask_btn = gr.Button("Ask", variant="primary")
            ask_btn.click(ask, inputs=prompt, outputs=answer, api_name="ask")

    return demo


demo = build_ui()

if __name__ == "__main__":
    demo.launch()