Spaces:
Runtime error
Runtime error
polish: add MIT license
Browse files- LICENSE +21 -0
- README.md +5 -1
- docs/ARCHITECTURE.md +103 -3
- docs/DEPLOY_GUIDE.md +124 -3
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Kevin Merchant
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -19,7 +19,7 @@ UI and capabilities β multi-turn chat with persistent short-term memory, a
|
|
| 19 |
calculator + web-search tool, and a two-layer input/output guardrail β and are
|
| 20 |
evaluated on hallucination, demographic bias, and safety / jailbreak resistance.
|
| 21 |
|
| 22 |
-
π **Live demo:**
|
| 23 |
π **Evaluation report:** [`docs/EVALUATION_REPORT.md`](docs/EVALUATION_REPORT.md)
|
| 24 |
π οΈ **Architecture notes:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) Β· **Deploy guide:** [`docs/DEPLOY_GUIDE.md`](docs/DEPLOY_GUIDE.md)
|
| 25 |
|
|
@@ -129,6 +129,10 @@ A 100-turn Claude conversation runs **~$0.50**; the same 100 turns on Qwen-via-Z
|
|
| 129 |
- Try a **larger OSS model** (Qwen2.5-7B or Llama-3.1-8B) on ZeroGPU's `xlarge` tier to quantify the OSSβfrontier gap-vs-size curve.
|
| 130 |
- Add **session-scoped Gradio state** (per-browser-tab session id) for true multi-user deployment.
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
## Project layout
|
| 133 |
|
| 134 |
```
|
|
|
|
| 19 |
calculator + web-search tool, and a two-layer input/output guardrail β and are
|
| 20 |
evaluated on hallucination, demographic bias, and safety / jailbreak resistance.
|
| 21 |
|
| 22 |
+
π **Live demo:** [huggingface.co/spaces/KevinMerchant13/oss-vs-frontier-assistant](https://huggingface.co/spaces/KevinMerchant13/oss-vs-frontier-assistant)
|
| 23 |
π **Evaluation report:** [`docs/EVALUATION_REPORT.md`](docs/EVALUATION_REPORT.md)
|
| 24 |
π οΈ **Architecture notes:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) Β· **Deploy guide:** [`docs/DEPLOY_GUIDE.md`](docs/DEPLOY_GUIDE.md)
|
| 25 |
|
|
|
|
| 129 |
- Try a **larger OSS model** (Qwen2.5-7B or Llama-3.1-8B) on ZeroGPU's `xlarge` tier to quantify the OSSβfrontier gap-vs-size curve.
|
| 130 |
- Add **session-scoped Gradio state** (per-browser-tab session id) for true multi-user deployment.
|
| 131 |
|
| 132 |
+
## License
|
| 133 |
+
|
| 134 |
+
[MIT](LICENSE) β free for any use with attribution.
|
| 135 |
+
|
| 136 |
## Project layout
|
| 137 |
|
| 138 |
```
|
docs/ARCHITECTURE.md
CHANGED
|
@@ -1,6 +1,106 @@
|
|
| 1 |
# Architecture
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Architecture
|
| 2 |
|
| 3 |
+
How the pieces fit together, and why each design decision was made.
|
| 4 |
|
| 5 |
+
## Request flow
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
browser
|
| 9 |
+
β
|
| 10 |
+
βΌ
|
| 11 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
+
β app.py β Gradio ChatInterface β
|
| 13 |
+
β β
|
| 14 |
+
β 1. input guardrail (regex blocklist) β
|
| 15 |
+
β ββ blocked β canned refusal + footer (model never called) β
|
| 16 |
+
β β
|
| 17 |
+
β 2. memory layer (per-turn) β
|
| 18 |
+
β RunnableWithMessageHistory.invoke({"input": msg}, session_id) β
|
| 19 |
+
β loads last 6 turns from SQLChatMessageHistory (SQLite) β
|
| 20 |
+
β β
|
| 21 |
+
β 3. assistant._respond β
|
| 22 |
+
β ββ SystemMessage + trimmed history + HumanMessage β
|
| 23 |
+
β ββ tool-calling loop (β€ 4 rounds): β
|
| 24 |
+
β model.invoke β if tool_calls β run_tool_call β repeat β
|
| 25 |
+
β β
|
| 26 |
+
β 4. output guardrail (Claude Haiku 4.5 moderation) β
|
| 27 |
+
β ββ blocked β refusal text, AND rewrite stored history β
|
| 28 |
+
β β
|
| 29 |
+
β 5. status footer (assistant | tools_used | guardrail states) β
|
| 30 |
+
β β
|
| 31 |
+
β Everything above is wrapped in a Langfuse @observe trace tagged β
|
| 32 |
+
β with session_id and assistant_type; tool/model spans nest under. β
|
| 33 |
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Module map
|
| 37 |
+
|
| 38 |
+
| File | Responsibility |
|
| 39 |
+
|-----------------------------------|--------------------------------------------------------------------------|
|
| 40 |
+
| `app.py` | Gradio entry; orchestrates guardrails β memory β assistant per turn. |
|
| 41 |
+
| `src/config.py` | pydantic-settings loader; drops empty env vars that would shadow `.env`. |
|
| 42 |
+
| `src/assistants/base.py` | `BaseAssistant` ABC; shared tool-calling loop + history trimming. |
|
| 43 |
+
| `src/assistants/frontier.py` | `ChatAnthropic` wrapper (Claude Sonnet 4.5). |
|
| 44 |
+
| `src/assistants/oss.py` | `QwenChatModel` β custom LangChain chat model with native tool template. |
|
| 45 |
+
| `src/memory.py` | SQLChatMessageHistory + `build_conversational()` factory. |
|
| 46 |
+
| `src/tools.py` | `calculator` (sandboxed AST eval) + `web_search` (Tavily). |
|
| 47 |
+
| `src/guardrails.py` | input regex blocklist + Haiku-4.5 output moderation. |
|
| 48 |
+
| `src/observability.py` | Langfuse client init + `@observe` decorator (no-op fallback). |
|
| 49 |
+
| `eval/datasets.py` | TruthfulQA / BBQ / AdvBench loaders, seed=42. |
|
| 50 |
+
| `eval/run_eval.py` | Resumable JSONL runner. |
|
| 51 |
+
| `eval/judge.py` | Claude Sonnet 4.5 LLM-as-judge with dataset-aware rubric. |
|
| 52 |
+
| `eval/report.py` | Bootstrap CIs, matplotlib charts, EVALUATION_REPORT.md. |
|
| 53 |
+
|
| 54 |
+
## Key design decisions and why
|
| 55 |
+
|
| 56 |
+
### 1. Why a single `BaseAssistant` with the tool-loop in the base class
|
| 57 |
+
|
| 58 |
+
Both assistants must have *identical capabilities* (per assessment spec) for the comparison to be fair. Putting the tool-calling loop, system prompt, history trimming, and memory plumbing in `BaseAssistant` means the only differences between Claude and Qwen are (a) the underlying LangChain chat model and (b) inference latency. Subclasses implement only `_build_model()`.
|
| 59 |
+
|
| 60 |
+
### 2. Why we built a custom `QwenChatModel` instead of using `ChatHuggingFace`
|
| 61 |
+
|
| 62 |
+
`langchain-huggingface.ChatHuggingFace.bind_tools()` does not render tool schemas into Qwen's chat template β so `bind_tools` is silently a no-op and Qwen never emits tool calls through it. Tested with a deliberate calculator question: Qwen wrote prose, never invoked the tool.
|
| 63 |
+
|
| 64 |
+
Qwen2.5-Instruct's *native* chat template fully supports tools and emits well-formed `<tool_call>{...}</tool_call>` blocks. `QwenChatModel`:
|
| 65 |
+
|
| 66 |
+
1. Overrides `bind_tools()` to attach OpenAI-style tool schemas to the runnable.
|
| 67 |
+
2. Calls `tokenizer.apply_chat_template(messages, tools=schemas, ...)` so Qwen sees the tools.
|
| 68 |
+
3. Parses `<tool_call>` blocks out of the output back into LangChain's `AIMessage.tool_calls` format.
|
| 69 |
+
|
| 70 |
+
Result: Qwen genuinely uses the calculator/search, matching the Claude interface.
|
| 71 |
+
|
| 72 |
+
### 3. Why guardrails live in the UI layer, not in the assistants
|
| 73 |
+
|
| 74 |
+
The evaluation must measure *raw* model behavior (per spec β that's how we honestly compare hallucination/bias/safety between OSS and frontier). If guardrails ran inside `assistant.chat()`, the eval would measure the *protected* system, not the model. So:
|
| 75 |
+
|
| 76 |
+
- `BaseAssistant.chat()` is stateless and unmoderated β used by the eval.
|
| 77 |
+
- `app.respond()` wraps that with input guardrail β memory invocation β output moderation β footer β used by the UI.
|
| 78 |
+
|
| 79 |
+
This keeps the eval honest while still demonstrating real guardrail behavior in the deployed app.
|
| 80 |
+
|
| 81 |
+
### 4. Why output moderation also rewrites stored history
|
| 82 |
+
|
| 83 |
+
A blocked unsafe reply, if persisted, would leak into the next turn's context and could prime the model. So on a block we both replace what the user sees and overwrite the last assistant message in `SQLChatMessageHistory` with the refusal. Rare path, so the `clear() + add_messages()` cycle is cheap.
|
| 84 |
+
|
| 85 |
+
### 5. Why `RunnableWithMessageHistory` + manual tool loop (rather than LangGraph)
|
| 86 |
+
|
| 87 |
+
`RunnableWithMessageHistory` is deprecated in LangChain 1.x in favor of LangGraph persistence β but the spec called for it explicitly, and adding `langgraph` would have meant a much larger dependency surface. The manual tool loop (capped at 4 rounds for safety) is ~15 lines, fully traceable, and easy to reason about.
|
| 88 |
+
|
| 89 |
+
### 6. Why a 6-turn memory window
|
| 90 |
+
|
| 91 |
+
Qwen-1.5B has a small effective context; we trim to the last 12 messages (6 user/assistant pairs) before each call so long sessions don't degrade Qwen's quality. SQLite still stores the full session (useful for audit/debugging); the trim is only at *read* time.
|
| 92 |
+
|
| 93 |
+
### 7. Why pydantic-settings drops empty env vars at config load
|
| 94 |
+
|
| 95 |
+
pydantic-settings ranks real environment variables above `.env`. The dev environment exported `ANTHROPIC_API_KEY=` (empty string) which silently shadowed the real key in `.env`. The config module deletes any empty-string entries for our known secrets so `.env` can fill them in. This is safe on Hugging Face Spaces β secrets there arrive as non-empty env vars and still win.
|
| 96 |
+
|
| 97 |
+
### 8. Why the judge uses a shared schema with per-dataset guidance
|
| 98 |
+
|
| 99 |
+
A single `{hallucinated, biased, refused, harmful, reasoning}` schema means all rows can be aggregated cleanly. But a one-size rubric would mis-grade: e.g., the "correct" BBQ ambig answer is *"Can't be determined"*, which a generic rubric might flag as a refusal. Per-dataset guidance in the prompt tells the judge which dimensions matter and how to interpret edge cases.
|
| 100 |
+
|
| 101 |
+
## Trade-offs accepted
|
| 102 |
+
|
| 103 |
+
- **`RunnableWithMessageHistory` deprecation**: future-LangChain incompatibility risk, but spec-required and matches existing tutorials.
|
| 104 |
+
- **Judge self-bias**: the judge is the same model family as one assistant under test. Disclosed in the report; mitigation would be a second judge or human spot-check on a subset.
|
| 105 |
+
- **No per-browser session id on Spaces**: a single process-global session id is used; fine for single-user demo, would need `gr.State` + cookie-derived id for genuine multi-user. Noted in README.
|
| 106 |
+
- **CPU-only deployment**: Qwen on shared CPU is slow. The `@spaces.GPU` decorator is in place so switching to ZeroGPU is a one-line YAML change once a PRO subscription is active.
|
docs/DEPLOY_GUIDE.md
CHANGED
|
@@ -1,6 +1,127 @@
|
|
| 1 |
# Deploy Guide
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Deploy Guide
|
| 2 |
|
| 3 |
+
End-to-end deployment of this project to a Hugging Face Space. Covers both the
|
| 4 |
+
**free CPU** path (used for this deploy) and the **ZeroGPU** path (for when a
|
| 5 |
+
HF PRO subscription is available).
|
| 6 |
|
| 7 |
+
## Prerequisites
|
| 8 |
+
|
| 9 |
+
- A Hugging Face account with an access token (User Settings β Access Tokens; needs **write** scope).
|
| 10 |
+
- The token in your local `.env` as `HF_TOKEN=hf_β¦`.
|
| 11 |
+
- The Python deps installed (`uv sync` in the project root).
|
| 12 |
+
|
| 13 |
+
## One-shot deploy via `huggingface_hub` Python API
|
| 14 |
+
|
| 15 |
+
Everything below β create the Space, set secrets, upload files β can be done in one Python script. This is what was actually run to produce the live demo.
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
from huggingface_hub import HfApi
|
| 19 |
+
from src.config import settings
|
| 20 |
+
|
| 21 |
+
REPO_ID = "<your-username>/oss-vs-frontier-assistant"
|
| 22 |
+
api = HfApi(token=settings.hf_token)
|
| 23 |
+
|
| 24 |
+
# 1) Create the Space (idempotent; safe to re-run).
|
| 25 |
+
api.create_repo(
|
| 26 |
+
repo_id=REPO_ID,
|
| 27 |
+
repo_type="space",
|
| 28 |
+
space_sdk="gradio",
|
| 29 |
+
space_hardware="cpu-basic", # or "zero-a10g" if you have HF PRO
|
| 30 |
+
exist_ok=True,
|
| 31 |
+
private=False,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# 2) Set every required secret. These show up as env vars in the Space runtime.
|
| 35 |
+
for k, v in {
|
| 36 |
+
"ANTHROPIC_API_KEY": settings.anthropic_api_key,
|
| 37 |
+
"HF_TOKEN": settings.hf_token,
|
| 38 |
+
"TAVILY_API_KEY": settings.tavily_api_key,
|
| 39 |
+
"LANGFUSE_PUBLIC_KEY": settings.langfuse_public_key,
|
| 40 |
+
"LANGFUSE_SECRET_KEY": settings.langfuse_secret_key,
|
| 41 |
+
"LANGFUSE_HOST": settings.langfuse_host,
|
| 42 |
+
}.items():
|
| 43 |
+
if v:
|
| 44 |
+
api.add_space_secret(repo_id=REPO_ID, key=k, value=v)
|
| 45 |
+
|
| 46 |
+
# 3) Upload the project, excluding local-only / sensitive files.
|
| 47 |
+
api.upload_folder(
|
| 48 |
+
repo_id=REPO_ID,
|
| 49 |
+
repo_type="space",
|
| 50 |
+
folder_path=".",
|
| 51 |
+
commit_message="deploy",
|
| 52 |
+
ignore_patterns=[
|
| 53 |
+
".env", ".git/**", ".venv/**", ".pytest_cache/**", ".claude/**",
|
| 54 |
+
"data/**", "results/**", "__pycache__/**", "**/__pycache__/**",
|
| 55 |
+
"*.pyc", ".gitignore", "uv.lock",
|
| 56 |
+
],
|
| 57 |
+
)
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Why this approach over the web UI:
|
| 61 |
+
|
| 62 |
+
- **No browser steps.** Reproducible from any machine with the token.
|
| 63 |
+
- **Secrets travel safely.** They never leave your machine in plaintext; the SDK posts them over HTTPS directly to the Space config.
|
| 64 |
+
- **Re-runnable.** `exist_ok=True` + `upload_folder` overwrite makes re-deploys trivial.
|
| 65 |
+
|
| 66 |
+
## What HF Spaces reads
|
| 67 |
+
|
| 68 |
+
| File | Role on Spaces |
|
| 69 |
+
|--------------|--------------------------------------------------------------------------------|
|
| 70 |
+
| `README.md` | The YAML frontmatter at the top configures the Space (sdk, hardware, etc.). |
|
| 71 |
+
| `requirements.txt` | Installed at build time. **Must be kept in sync with `pyproject.toml`.** |
|
| 72 |
+
| `app.py` | Entry point (`app_file: app.py` in the YAML); HF imports it and finds `demo`. |
|
| 73 |
+
|
| 74 |
+
The YAML frontmatter currently used:
|
| 75 |
+
|
| 76 |
+
```yaml
|
| 77 |
+
---
|
| 78 |
+
title: OSS vs Frontier Assistant
|
| 79 |
+
emoji: π€
|
| 80 |
+
colorFrom: indigo
|
| 81 |
+
colorTo: purple
|
| 82 |
+
sdk: gradio
|
| 83 |
+
sdk_version: 6.14.0
|
| 84 |
+
python_version: "3.11"
|
| 85 |
+
app_file: app.py
|
| 86 |
+
hardware: cpu-basic
|
| 87 |
+
pinned: false
|
| 88 |
+
---
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
## Switching to ZeroGPU later
|
| 92 |
+
|
| 93 |
+
1. Subscribe to HF PRO ($9/mo).
|
| 94 |
+
2. In the Space's **Settings β Hardware**, switch to `Nvidia A10G - Zero` (or rerun the deploy script with `space_hardware="zero-a10g"`).
|
| 95 |
+
3. Update the YAML in `README.md` to `hardware: zero-a10g`.
|
| 96 |
+
4. Re-upload: the `@spaces.GPU(duration=120)` decorator already on `QwenChatModel._generate` will start allocating real GPU time β Qwen latency drops from ~30-60s to ~3-8s per reply.
|
| 97 |
+
|
| 98 |
+
## Re-deploy after code changes
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
# Bump requirements.txt if pyproject.toml changed, then:
|
| 102 |
+
uv run python - <<'PY'
|
| 103 |
+
from huggingface_hub import HfApi
|
| 104 |
+
from src.config import settings
|
| 105 |
+
HfApi(token=settings.hf_token).upload_folder(
|
| 106 |
+
repo_id="<your-username>/oss-vs-frontier-assistant",
|
| 107 |
+
repo_type="space",
|
| 108 |
+
folder_path=".",
|
| 109 |
+
commit_message="update",
|
| 110 |
+
ignore_patterns=[".env", ".git/**", ".venv/**", ".pytest_cache/**", ".claude/**",
|
| 111 |
+
"data/**", "results/**", "__pycache__/**", "**/__pycache__/**",
|
| 112 |
+
"*.pyc", ".gitignore", "uv.lock"],
|
| 113 |
+
)
|
| 114 |
+
PY
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
HF triggers a new build automatically when files change.
|
| 118 |
+
|
| 119 |
+
## Troubleshooting
|
| 120 |
+
|
| 121 |
+
| Symptom on Spaces | Likely cause / fix |
|
| 122 |
+
|---------------------------------------------|-------------------------------------------------------------------------------|
|
| 123 |
+
| Build fails on `torch`/`transformers` install | Mismatch between `requirements.txt` pin and HF base image β check `python_version`. |
|
| 124 |
+
| `ANTHROPIC_API_KEY is not set` at runtime | Secret not added in Space settings, or empty. Re-run the secrets loop above. |
|
| 125 |
+
| 403 on `create_repo` mentioning ZeroGPU | ZeroGPU is gated behind HF PRO; use `space_hardware="cpu-basic"` instead. |
|
| 126 |
+
| Qwen replies very slowly (30-60s) | Expected on `cpu-basic`. Switch to ZeroGPU per the section above. |
|
| 127 |
+
| Tracing missing from Langfuse | Network timeout on the Space β traces. Non-fatal; bump `LANGFUSE_TIMEOUT=30`. |
|