Upload folder using huggingface_hub
Browse files- .gitattributes +35 -35
- .gitignore +42 -0
- README.md +121 -2
- app.py +354 -0
- chat_logger.py +143 -0
- docs/IMPLEMENTATION_SUMMARY.md +89 -0
- docs/LOGGING.md +153 -0
- requirements.txt +1 -0
- run.ps1 +6 -0
- styles.css +179 -0
- theme.py +136 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,35 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual environments
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
env/
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
*$py.class
|
| 10 |
+
*.so
|
| 11 |
+
.Python
|
| 12 |
+
*.egg
|
| 13 |
+
*.egg-info/
|
| 14 |
+
.eggs/
|
| 15 |
+
dist/
|
| 16 |
+
build/
|
| 17 |
+
pip-wheel-metadata/
|
| 18 |
+
|
| 19 |
+
# Testing / type check / lint caches
|
| 20 |
+
.pytest_cache/
|
| 21 |
+
.mypy_cache/
|
| 22 |
+
.ruff_cache/
|
| 23 |
+
.coverage
|
| 24 |
+
htmlcov/
|
| 25 |
+
|
| 26 |
+
# Gradio / local runs
|
| 27 |
+
.gradio/
|
| 28 |
+
flagged/
|
| 29 |
+
|
| 30 |
+
# Environment
|
| 31 |
+
.env
|
| 32 |
+
.env.*
|
| 33 |
+
!.env.example
|
| 34 |
+
|
| 35 |
+
# IDE / OS
|
| 36 |
+
.idea/
|
| 37 |
+
.vscode/
|
| 38 |
+
*.swp
|
| 39 |
+
.DS_Store
|
| 40 |
+
Thumbs.db
|
| 41 |
+
|
| 42 |
+
logs/
|
README.md
CHANGED
|
@@ -7,7 +7,126 @@ sdk: gradio
|
|
| 7 |
sdk_version: 6.11.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
short_description: Sequential Thinking MCP Server
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
sdk_version: 6.11.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
short_description: Sequential Thinking MCP Server (Gradio + Python)
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Sequential Thinking — Gradio MCP Server
|
| 14 |
+
|
| 15 |
+
Python [Gradio](https://www.gradio.app/) app that exposes **Sequential Thinking** as an MCP server over SSE, alongside a small UI for manual thought steps. It mirrors the behaviour of the reference [Sequential Thinking MCP server](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking) (TypeScript) using Gradio’s built-in MCP support (`gradio[mcp]`).
|
| 16 |
+
|
| 17 |
+
## What’s included
|
| 18 |
+
|
| 19 |
+
- **MCP over HTTP (SSE)** — connect any MCP client to the Gradio `/gradio_api/mcp/sse` endpoint
|
| 20 |
+
- **Web UI** — process thoughts, reset session, inspect history, and copy-paste MCP config
|
| 21 |
+
- **Structured thinking** — revisions, branches, adjustable totals, optional “needs more thoughts”
|
| 22 |
+
- **Logging** — every thought is appended to `logs/chat_logs.jsonl`; optional periodic uploads to a Hugging Face dataset when `HF_TOKEN` and `HF_DATASET_REPO` are set (see [docs/LOGGING.md](docs/LOGGING.md))
|
| 23 |
+
|
| 24 |
+
## Requirements
|
| 25 |
+
|
| 26 |
+
- Python 3.10+ recommended
|
| 27 |
+
- Dependencies: see [requirements.txt](requirements.txt) (`gradio[mcp]==6.11.0`)
|
| 28 |
+
|
| 29 |
+
## Run locally
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
python -m venv .venv
|
| 33 |
+
# Windows: .venv\Scripts\activate
|
| 34 |
+
# macOS/Linux: source .venv/bin/activate
|
| 35 |
+
pip install -r requirements.txt
|
| 36 |
+
python app.py
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Gradio starts (default [http://localhost:7860](http://localhost:7860)). **MCP SSE URL:**
|
| 40 |
+
|
| 41 |
+
`http://localhost:7860/gradio_api/mcp/sse`
|
| 42 |
+
|
| 43 |
+
On Windows, if your default console encoding breaks Gradio’s startup output, use UTF-8, for example:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
python -X utf8 app.py
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
**PowerShell helper:** [run.ps1](run.ps1) expects a virtualenv folder named `venv` at the repo root (`venv\Scripts\python.exe`). If you use `.venv` instead, activate it and run `python app.py` as above.
|
| 50 |
+
|
| 51 |
+
## MCP tools
|
| 52 |
+
|
| 53 |
+
| Tool | Description |
|
| 54 |
+
| --------------------- | ---------------------------------------------------------------- |
|
| 55 |
+
| `sequential_thinking` | Record one thinking step and return progress metadata |
|
| 56 |
+
| `reset_session` | Clear in-memory history and branches; new session id for logging |
|
| 57 |
+
| `get_history` | Return the full thought list for the current session |
|
| 58 |
+
|
| 59 |
+
### `sequential_thinking` parameters
|
| 60 |
+
|
| 61 |
+
Names follow the Python implementation (snake_case):
|
| 62 |
+
|
| 63 |
+
| Parameter | Type | Required | Description |
|
| 64 |
+
| --------------------- | ------- | -------- | ---------------------------------------- |
|
| 65 |
+
| `thought` | string | yes | Current thinking step |
|
| 66 |
+
| `thought_number` | integer | yes | Step index (starts at 1) |
|
| 67 |
+
| `total_thoughts` | integer | yes | Estimated total steps (can grow) |
|
| 68 |
+
| `next_thought_needed` | boolean | yes | Whether another step is expected |
|
| 69 |
+
| `is_revision` | boolean | no | This step revises earlier thinking |
|
| 70 |
+
| `revises_thought` | integer | no | Which thought number is revised |
|
| 71 |
+
| `branch_from_thought` | integer | no | Branch from this thought number |
|
| 72 |
+
| `branch_id` | string | no | Label for the branch |
|
| 73 |
+
| `needs_more_thoughts` | boolean | no | Extend beyond the current total estimate |
|
| 74 |
+
|
| 75 |
+
## Configure your MCP client
|
| 76 |
+
|
| 77 |
+
Point the client at the SSE URL (adjust host/port if you change Gradio’s `server_name` / `server_port`).
|
| 78 |
+
|
| 79 |
+
### Claude Desktop (`claude_desktop_config.json`)
|
| 80 |
+
|
| 81 |
+
```json
|
| 82 |
+
{
|
| 83 |
+
"mcpServers": {
|
| 84 |
+
"sequential-thinking": {
|
| 85 |
+
"url": "http://localhost:7860/gradio_api/mcp/sse"
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### VS Code — user or workspace `mcp.json`
|
| 92 |
+
|
| 93 |
+
```json
|
| 94 |
+
{
|
| 95 |
+
"servers": {
|
| 96 |
+
"sequential-thinking": {
|
| 97 |
+
"url": "http://localhost:7860/gradio_api/mcp/sse"
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
See [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) for file locations.
|
| 104 |
+
|
| 105 |
+
### Cursor
|
| 106 |
+
|
| 107 |
+
Add an MCP server entry with the same `url` as above (for example in your Cursor MCP configuration), with the app running locally.
|
| 108 |
+
|
| 109 |
+
## Environment variables
|
| 110 |
+
|
| 111 |
+
| Variable | Effect |
|
| 112 |
+
| ------------------------- | ------------------------------------------------------------------------------------------- |
|
| 113 |
+
| `DISABLE_THOUGHT_LOGGING` | Set to `true` to suppress **console** banners for each thought (JSONL logging is unchanged) |
|
| 114 |
+
| `HF_TOKEN` | Optional — Hugging Face token for dataset upload |
|
| 115 |
+
| `HF_DATASET_REPO` | Optional — dataset id (e.g. `username/my-logs`) used with `HF_TOKEN` |
|
| 116 |
+
|
| 117 |
+
Details: [docs/LOGGING.md](docs/LOGGING.md).
|
| 118 |
+
|
| 119 |
+
## Hugging Face Spaces
|
| 120 |
+
|
| 121 |
+
The YAML front matter at the top of this file is for Space deployment. After deploy, use your Space URL for MCP, for example:
|
| 122 |
+
|
| 123 |
+
`https://<your-space-name>.hf.space/gradio_api/mcp/sse`
|
| 124 |
+
|
| 125 |
+
## Relation to other packages
|
| 126 |
+
|
| 127 |
+
The npm package `@modelcontextprotocol/server-sequential-thinking` and the `mcp/sequentialthinking` Docker image are **separate** official distributions. **This repository** is source for the Gradio/Python app only; it does not ship a Dockerfile or Node build in-tree.
|
| 128 |
+
|
| 129 |
+
## Docs
|
| 130 |
+
|
| 131 |
+
- [docs/LOGGING.md](docs/LOGGING.md) — log format, Hugging Face upload, troubleshooting
|
| 132 |
+
- [docs/IMPLEMENTATION_SUMMARY.md](docs/IMPLEMENTATION_SUMMARY.md) — logging implementation notes
|
app.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sequential Thinking — Gradio MCP App
|
| 3 |
+
|
| 4 |
+
Ports the Sequential Thinking MCP Server (TypeScript) to a Python Gradio app
|
| 5 |
+
that exposes the same tool via Gradio's built-in MCP server support.
|
| 6 |
+
|
| 7 |
+
Launch: python app.py
|
| 8 |
+
MCP SSE endpoint: http://localhost:7860/gradio_api/mcp/sse
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import sys
|
| 14 |
+
from typing import Optional
|
| 15 |
+
from uuid import uuid4
|
| 16 |
+
|
| 17 |
+
# On Windows the default console encoding (cp1252) cannot represent the emoji
|
| 18 |
+
# characters that Gradio prints on startup. Reconfigure stdout/stderr to
|
| 19 |
+
# UTF-8 early so those prints succeed. When running via `run.ps1` (or with
|
| 20 |
+
# `python -X utf8 app.py`) the streams are already UTF-8 and this is a no-op.
|
| 21 |
+
if sys.platform == "win32":
|
| 22 |
+
for _stream in (sys.stdout, sys.stderr):
|
| 23 |
+
try:
|
| 24 |
+
if hasattr(_stream, "reconfigure") and _stream.encoding.lower() != "utf-8":
|
| 25 |
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
| 26 |
+
except Exception:
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
import gradio as gr
|
| 30 |
+
from theme import soft_professional_theme
|
| 31 |
+
from chat_logger import log_chat, shutdown_logger
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# Core state (mirrors SequentialThinkingServer in lib.ts)
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
_thought_history: list[dict] = []
|
| 38 |
+
_branches: dict[str, list[dict]] = {}
|
| 39 |
+
_session_id: str = uuid4().hex # Session identifier for logging
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ---------------------------------------------------------------------------
|
| 43 |
+
# Tool logic
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
|
| 46 |
+
def sequential_thinking(
|
| 47 |
+
thought: str,
|
| 48 |
+
thought_number: int,
|
| 49 |
+
total_thoughts: int,
|
| 50 |
+
next_thought_needed: bool,
|
| 51 |
+
is_revision: bool = False,
|
| 52 |
+
revises_thought: Optional[int] = None,
|
| 53 |
+
branch_from_thought: Optional[int] = None,
|
| 54 |
+
branch_id: Optional[str] = None,
|
| 55 |
+
needs_more_thoughts: bool = False,
|
| 56 |
+
) -> dict:
|
| 57 |
+
"""
|
| 58 |
+
A detailed tool for dynamic and reflective problem-solving through thoughts.
|
| 59 |
+
This tool helps analyze problems through a flexible thinking process that
|
| 60 |
+
can adapt and evolve. Each thought can build on, question, or revise
|
| 61 |
+
previous insights as understanding deepens.
|
| 62 |
+
|
| 63 |
+
Use this tool when:
|
| 64 |
+
- Breaking down complex problems into steps
|
| 65 |
+
- Planning and design with room for revision
|
| 66 |
+
- Analysis that might need course correction
|
| 67 |
+
- Problems where the full scope is not clear initially
|
| 68 |
+
- Tasks that need context maintained over multiple steps
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
thought: Your current thinking step (analytical step, revision, question, hypothesis, etc.)
|
| 72 |
+
thought_number: Current thought number in sequence (starts at 1)
|
| 73 |
+
total_thoughts: Estimated total thoughts needed — can be adjusted up or down
|
| 74 |
+
next_thought_needed: True if more thinking is required, even if at what seemed like the end
|
| 75 |
+
is_revision: True if this thought revises or corrects a previous thought
|
| 76 |
+
revises_thought: Which thought number is being reconsidered (required when is_revision=True)
|
| 77 |
+
branch_from_thought: Thought number this branch splits off from (for alternative paths)
|
| 78 |
+
branch_id: Unique identifier for this branch (e.g. "alternative-approach")
|
| 79 |
+
needs_more_thoughts: True if you realise more thoughts are needed beyond totalThoughts
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
dict with thoughtNumber, totalThoughts, nextThoughtNeeded, branches, thoughtHistoryLength
|
| 83 |
+
"""
|
| 84 |
+
global _thought_history, _branches
|
| 85 |
+
|
| 86 |
+
# Sanitise optional int inputs that arrive as 0 from Gradio Number widgets
|
| 87 |
+
if revises_thought == 0:
|
| 88 |
+
revises_thought = None
|
| 89 |
+
if branch_from_thought == 0:
|
| 90 |
+
branch_from_thought = None
|
| 91 |
+
|
| 92 |
+
# Mirror TypeScript behaviour: expand totalThoughts if thoughtNumber exceeds it
|
| 93 |
+
if thought_number > total_thoughts:
|
| 94 |
+
total_thoughts = thought_number
|
| 95 |
+
|
| 96 |
+
thought_data: dict = {
|
| 97 |
+
"thought": thought,
|
| 98 |
+
"thoughtNumber": thought_number,
|
| 99 |
+
"totalThoughts": total_thoughts,
|
| 100 |
+
"isRevision": is_revision,
|
| 101 |
+
"revisesThought": revises_thought,
|
| 102 |
+
"branchFromThought": branch_from_thought,
|
| 103 |
+
"branchId": branch_id if branch_id and branch_id.strip() else None,
|
| 104 |
+
"needsMoreThoughts": needs_more_thoughts,
|
| 105 |
+
"nextThoughtNeeded": next_thought_needed,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
_thought_history.append(thought_data)
|
| 109 |
+
|
| 110 |
+
if branch_from_thought and branch_id and branch_id.strip():
|
| 111 |
+
_branches.setdefault(branch_id, []).append(thought_data)
|
| 112 |
+
|
| 113 |
+
_log_thought(thought_data)
|
| 114 |
+
|
| 115 |
+
# Log to local storage
|
| 116 |
+
log_chat(
|
| 117 |
+
session_id=_session_id,
|
| 118 |
+
model_name="sequential-thinking",
|
| 119 |
+
thought=thought,
|
| 120 |
+
thought_number=thought_number,
|
| 121 |
+
total_thoughts=total_thoughts,
|
| 122 |
+
metadata={
|
| 123 |
+
"is_revision": is_revision,
|
| 124 |
+
"revises_thought": revises_thought,
|
| 125 |
+
"branch_from_thought": branch_from_thought,
|
| 126 |
+
"branch_id": branch_id,
|
| 127 |
+
"needs_more_thoughts": needs_more_thoughts,
|
| 128 |
+
"next_thought_needed": next_thought_needed,
|
| 129 |
+
}
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"thoughtNumber": thought_number,
|
| 134 |
+
"totalThoughts": total_thoughts,
|
| 135 |
+
"nextThoughtNeeded": next_thought_needed,
|
| 136 |
+
"branches": list(_branches.keys()),
|
| 137 |
+
"thoughtHistoryLength": len(_thought_history),
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def reset_session() -> str:
|
| 142 |
+
"""
|
| 143 |
+
Reset the thought history and all branches, starting a fresh problem-solving session.
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
Confirmation message string.
|
| 147 |
+
"""
|
| 148 |
+
global _thought_history, _branches, _session_id
|
| 149 |
+
_thought_history = []
|
| 150 |
+
_branches = {}
|
| 151 |
+
_session_id = uuid4().hex # Generate new session ID
|
| 152 |
+
return "Session reset. Ready for a new problem-solving session."
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def get_history() -> list:
|
| 156 |
+
"""
|
| 157 |
+
Return the full thought history for the current session.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
List of all recorded thought objects.
|
| 161 |
+
"""
|
| 162 |
+
return _thought_history
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# ---------------------------------------------------------------------------
|
| 166 |
+
# Logging helper (mirrors lib.ts formatThought, without chalk colours)
|
| 167 |
+
# ---------------------------------------------------------------------------
|
| 168 |
+
|
| 169 |
+
def _log_thought(data: dict) -> None:
|
| 170 |
+
if os.environ.get("DISABLE_THOUGHT_LOGGING", "").lower() == "true":
|
| 171 |
+
return
|
| 172 |
+
|
| 173 |
+
t_num = data["thoughtNumber"]
|
| 174 |
+
t_total = data["totalThoughts"]
|
| 175 |
+
thought = data["thought"]
|
| 176 |
+
|
| 177 |
+
if data.get("isRevision"):
|
| 178 |
+
label = f"[Revision] {t_num}/{t_total} (revising thought {data.get('revisesThought')})"
|
| 179 |
+
elif data.get("branchFromThought"):
|
| 180 |
+
label = (
|
| 181 |
+
f"[Branch] {t_num}/{t_total} "
|
| 182 |
+
f"(from thought {data['branchFromThought']}, ID: {data.get('branchId')})"
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
label = f"[Thought] {t_num}/{t_total}"
|
| 186 |
+
|
| 187 |
+
width = max(len(label), len(thought)) + 4
|
| 188 |
+
border = "-" * width
|
| 189 |
+
try:
|
| 190 |
+
print(f"\n+{border}+")
|
| 191 |
+
print(f"| {label.ljust(width - 2)} |")
|
| 192 |
+
print(f"+{border}+")
|
| 193 |
+
print(f"| {thought.ljust(width - 2)} |")
|
| 194 |
+
print(f"+{border}+")
|
| 195 |
+
except UnicodeEncodeError:
|
| 196 |
+
print(f"\n{label}\n {thought}")
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# ---------------------------------------------------------------------------
|
| 200 |
+
# Gradio UI
|
| 201 |
+
# ---------------------------------------------------------------------------
|
| 202 |
+
|
| 203 |
+
DESCRIPTION = """
|
| 204 |
+
# 🧠 Sequential Thinking MCP Server
|
| 205 |
+
|
| 206 |
+
Dynamic, reflective problem-solving through structured thoughts.
|
| 207 |
+
This Gradio app exposes the **Sequential Thinking** tool as an MCP server.
|
| 208 |
+
|
| 209 |
+
**MCP endpoint (SSE):** `http://localhost:7860/gradio_api/mcp/sse`
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
# Load custom CSS
|
| 213 |
+
with open('styles.css', 'r') as f:
|
| 214 |
+
custom_css = f.read()
|
| 215 |
+
|
| 216 |
+
with gr.Blocks(title="Sequential Thinking MCP", theme=soft_professional_theme, css=custom_css) as demo:
|
| 217 |
+
gr.Markdown(DESCRIPTION)
|
| 218 |
+
|
| 219 |
+
with gr.Tabs():
|
| 220 |
+
|
| 221 |
+
# ------------------------------------------------------------------
|
| 222 |
+
# Tab 1 — process a thought
|
| 223 |
+
# ------------------------------------------------------------------
|
| 224 |
+
with gr.Tab("Process Thought"):
|
| 225 |
+
with gr.Row(equal_height=False):
|
| 226 |
+
with gr.Column(scale=1):
|
| 227 |
+
thought_input = gr.Textbox(
|
| 228 |
+
label="Thought",
|
| 229 |
+
placeholder="Enter your current thinking step…",
|
| 230 |
+
lines=5,
|
| 231 |
+
)
|
| 232 |
+
with gr.Row():
|
| 233 |
+
thought_number_input = gr.Number(
|
| 234 |
+
label="Thought Number", value=1, minimum=1, precision=0
|
| 235 |
+
)
|
| 236 |
+
total_thoughts_input = gr.Number(
|
| 237 |
+
label="Total Thoughts (estimate)", value=5, minimum=1, precision=0
|
| 238 |
+
)
|
| 239 |
+
next_thought_needed_input = gr.Checkbox(
|
| 240 |
+
label="Next Thought Needed", value=True
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
with gr.Accordion("Advanced options", open=False):
|
| 244 |
+
is_revision_input = gr.Checkbox(label="Is Revision", value=False)
|
| 245 |
+
revises_thought_input = gr.Number(
|
| 246 |
+
label="Revises Thought #",
|
| 247 |
+
minimum=0,
|
| 248 |
+
precision=0,
|
| 249 |
+
value=0,
|
| 250 |
+
info="0 = not applicable",
|
| 251 |
+
)
|
| 252 |
+
branch_from_thought_input = gr.Number(
|
| 253 |
+
label="Branch From Thought #",
|
| 254 |
+
minimum=0,
|
| 255 |
+
precision=0,
|
| 256 |
+
value=0,
|
| 257 |
+
info="0 = not applicable",
|
| 258 |
+
)
|
| 259 |
+
branch_id_input = gr.Textbox(
|
| 260 |
+
label="Branch ID",
|
| 261 |
+
placeholder="e.g. alternative-approach",
|
| 262 |
+
)
|
| 263 |
+
needs_more_thoughts_input = gr.Checkbox(
|
| 264 |
+
label="Needs More Thoughts", value=False
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
with gr.Row():
|
| 268 |
+
submit_btn = gr.Button("Process Thought", variant="primary")
|
| 269 |
+
reset_btn = gr.Button("Reset Session", variant="secondary")
|
| 270 |
+
|
| 271 |
+
with gr.Column(scale=1):
|
| 272 |
+
output_json = gr.JSON(label="Result")
|
| 273 |
+
status_box = gr.Textbox(label="Status", interactive=False, visible=True)
|
| 274 |
+
|
| 275 |
+
submit_btn.click(
|
| 276 |
+
fn=sequential_thinking,
|
| 277 |
+
inputs=[
|
| 278 |
+
thought_input,
|
| 279 |
+
thought_number_input,
|
| 280 |
+
total_thoughts_input,
|
| 281 |
+
next_thought_needed_input,
|
| 282 |
+
is_revision_input,
|
| 283 |
+
revises_thought_input,
|
| 284 |
+
branch_from_thought_input,
|
| 285 |
+
branch_id_input,
|
| 286 |
+
needs_more_thoughts_input,
|
| 287 |
+
],
|
| 288 |
+
outputs=output_json,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
reset_btn.click(fn=reset_session, inputs=[], outputs=status_box)
|
| 292 |
+
|
| 293 |
+
# ------------------------------------------------------------------
|
| 294 |
+
# Tab 2 — inspect history
|
| 295 |
+
# ------------------------------------------------------------------
|
| 296 |
+
with gr.Tab("Thought History"):
|
| 297 |
+
refresh_btn = gr.Button("Refresh")
|
| 298 |
+
history_output = gr.JSON(label="All Thoughts")
|
| 299 |
+
refresh_btn.click(fn=get_history, inputs=[], outputs=history_output)
|
| 300 |
+
|
| 301 |
+
# ------------------------------------------------------------------
|
| 302 |
+
# Tab 3 — quick reference
|
| 303 |
+
# ------------------------------------------------------------------
|
| 304 |
+
with gr.Tab("MCP Reference"):
|
| 305 |
+
gr.Markdown("""
|
| 306 |
+
## Connecting via MCP
|
| 307 |
+
|
| 308 |
+
Add this to your MCP client configuration:
|
| 309 |
+
|
| 310 |
+
```json
|
| 311 |
+
{
|
| 312 |
+
"mcpServers": {
|
| 313 |
+
"sequential-thinking": {
|
| 314 |
+
"url": "http://localhost:7860/gradio_api/mcp/sse"
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
```
|
| 319 |
+
|
| 320 |
+
## Exposed MCP Tools
|
| 321 |
+
|
| 322 |
+
| Tool | Description |
|
| 323 |
+
|------|-------------|
|
| 324 |
+
| `sequential_thinking` | Process a single thought step |
|
| 325 |
+
| `reset_session` | Clear history and branches |
|
| 326 |
+
| `get_history` | Return the full thought history |
|
| 327 |
+
|
| 328 |
+
## Parameters for `sequential_thinking`
|
| 329 |
+
|
| 330 |
+
| Parameter | Type | Required | Description |
|
| 331 |
+
|-----------|------|----------|-------------|
|
| 332 |
+
| `thought` | string | ✅ | Current thinking step |
|
| 333 |
+
| `thought_number` | int | ✅ | Step index (starts at 1) |
|
| 334 |
+
| `total_thoughts` | int | ✅ | Estimated total steps |
|
| 335 |
+
| `next_thought_needed` | bool | ✅ | More steps required? |
|
| 336 |
+
| `is_revision` | bool | | Revises an earlier thought |
|
| 337 |
+
| `revises_thought` | int | | Which thought is revised |
|
| 338 |
+
| `branch_from_thought` | int | | Branch origin thought |
|
| 339 |
+
| `branch_id` | string | | Branch label |
|
| 340 |
+
| `needs_more_thoughts` | bool | | Extend beyond estimate |
|
| 341 |
+
|
| 342 |
+
## Environment Variables
|
| 343 |
+
|
| 344 |
+
| Variable | Default | Description |
|
| 345 |
+
|----------|---------|-------------|
|
| 346 |
+
| `DISABLE_THOUGHT_LOGGING` | `false` | Set to `true` to suppress console output |
|
| 347 |
+
""")
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
if __name__ == "__main__":
|
| 351 |
+
try:
|
| 352 |
+
demo.launch(mcp_server=True)
|
| 353 |
+
finally:
|
| 354 |
+
shutdown_logger()
|
chat_logger.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chat logging module with local JSONL storage and optional HuggingFace upload."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
import threading
|
| 6 |
+
import atexit
|
| 7 |
+
from queue import Queue
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Optional
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from huggingface_hub import HfApi
|
| 14 |
+
HF_AVAILABLE = True
|
| 15 |
+
except ImportError:
|
| 16 |
+
HF_AVAILABLE = False
|
| 17 |
+
|
| 18 |
+
# Configuration
|
| 19 |
+
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 20 |
+
DATASET_REPO_ID = os.environ.get("HF_DATASET_REPO")
|
| 21 |
+
LOCAL_LOG_DIR = Path("logs")
|
| 22 |
+
LOCAL_LOG_FILE = LOCAL_LOG_DIR / "chat_logs.jsonl"
|
| 23 |
+
UPLOAD_INTERVAL_SECONDS = 300 # 5 minutes
|
| 24 |
+
|
| 25 |
+
# Ensure log directory exists
|
| 26 |
+
LOCAL_LOG_DIR.mkdir(exist_ok=True)
|
| 27 |
+
|
| 28 |
+
# Thread-safe queue for async logging
|
| 29 |
+
_log_queue: Queue = Queue()
|
| 30 |
+
_shutdown_event = threading.Event()
|
| 31 |
+
_upload_lock = threading.Lock()
|
| 32 |
+
_worker_thread_started = False
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def log_chat(
|
| 36 |
+
session_id: str,
|
| 37 |
+
model_name: str,
|
| 38 |
+
thought: str,
|
| 39 |
+
thought_number: int,
|
| 40 |
+
total_thoughts: int,
|
| 41 |
+
metadata: Optional[dict] = None
|
| 42 |
+
) -> None:
|
| 43 |
+
"""Queue a thought log entry for async processing."""
|
| 44 |
+
_log_queue.put({
|
| 45 |
+
"session_id": session_id,
|
| 46 |
+
"model_name": model_name,
|
| 47 |
+
"thought": thought,
|
| 48 |
+
"thought_number": thought_number,
|
| 49 |
+
"total_thoughts": total_thoughts,
|
| 50 |
+
"metadata": metadata or {},
|
| 51 |
+
"timestamp": datetime.now().isoformat()
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _write_to_jsonl(entry: dict) -> None:
|
| 56 |
+
"""Append a log entry to the local JSONL file."""
|
| 57 |
+
try:
|
| 58 |
+
with open(LOCAL_LOG_FILE, "a", encoding="utf-8") as f:
|
| 59 |
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Error writing to JSONL: {e}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _worker_thread():
|
| 65 |
+
"""Background worker that processes the log queue."""
|
| 66 |
+
while not _shutdown_event.is_set():
|
| 67 |
+
try:
|
| 68 |
+
entry = _log_queue.get(timeout=1.0)
|
| 69 |
+
_write_to_jsonl(entry)
|
| 70 |
+
_log_queue.task_done()
|
| 71 |
+
except Exception:
|
| 72 |
+
continue
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _upload_to_huggingface() -> bool:
|
| 76 |
+
"""Upload local JSONL to HuggingFace dataset (if configured)."""
|
| 77 |
+
if not HF_AVAILABLE or not HF_TOKEN or not DATASET_REPO_ID:
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
with _upload_lock:
|
| 81 |
+
if not LOCAL_LOG_FILE.exists():
|
| 82 |
+
return False
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
api = HfApi(token=HF_TOKEN)
|
| 86 |
+
|
| 87 |
+
# Ensure repo exists
|
| 88 |
+
try:
|
| 89 |
+
api.repo_info(repo_id=DATASET_REPO_ID, repo_type="dataset")
|
| 90 |
+
except Exception:
|
| 91 |
+
api.create_repo(
|
| 92 |
+
repo_id=DATASET_REPO_ID,
|
| 93 |
+
repo_type="dataset",
|
| 94 |
+
private=True
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# Upload with timestamp
|
| 98 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 99 |
+
api.upload_file(
|
| 100 |
+
path_or_fileobj=str(LOCAL_LOG_FILE),
|
| 101 |
+
path_in_repo=f"logs/chat_logs_{timestamp}.jsonl",
|
| 102 |
+
repo_id=DATASET_REPO_ID,
|
| 103 |
+
repo_type="dataset",
|
| 104 |
+
commit_message=f"Chat logs upload {timestamp}"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# Clear local file after successful upload
|
| 108 |
+
LOCAL_LOG_FILE.unlink()
|
| 109 |
+
LOCAL_LOG_FILE.touch()
|
| 110 |
+
return True
|
| 111 |
+
except Exception as e:
|
| 112 |
+
print(f"HuggingFace upload failed: {e}")
|
| 113 |
+
return False
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _upload_timer_thread():
|
| 117 |
+
"""Periodic upload thread."""
|
| 118 |
+
while not _shutdown_event.is_set():
|
| 119 |
+
_shutdown_event.wait(timeout=UPLOAD_INTERVAL_SECONDS)
|
| 120 |
+
if not _shutdown_event.is_set():
|
| 121 |
+
_upload_to_huggingface()
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def shutdown_logger():
|
| 125 |
+
"""Gracefully shutdown logger and upload remaining logs."""
|
| 126 |
+
_shutdown_event.set()
|
| 127 |
+
_log_queue.join() # Wait for queue to empty
|
| 128 |
+
_upload_to_huggingface() # Final upload
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _ensure_threads_started():
|
| 132 |
+
"""Start background threads if not already started."""
|
| 133 |
+
global _worker_thread_started
|
| 134 |
+
if not _worker_thread_started:
|
| 135 |
+
_worker_thread_started = True
|
| 136 |
+
threading.Thread(target=_worker_thread, daemon=True).start()
|
| 137 |
+
if HF_AVAILABLE and HF_TOKEN and DATASET_REPO_ID:
|
| 138 |
+
threading.Thread(target=_upload_timer_thread, daemon=True).start()
|
| 139 |
+
atexit.register(shutdown_logger)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# Ensure threads start when module is imported
|
| 143 |
+
_ensure_threads_started()
|
docs/IMPLEMENTATION_SUMMARY.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Chat Logging Implementation Summary
|
| 2 |
+
|
| 3 |
+
## Changes Made
|
| 4 |
+
|
| 5 |
+
### 1. **New File: `chat_logger.py`**
|
| 6 |
+
- Implements local JSONL chat logging with thread-safe async processing
|
| 7 |
+
- **Local logging**: Always enabled, writes to `logs/chat_logs.jsonl`
|
| 8 |
+
- **Optional HuggingFace upload**: Only if `HF_TOKEN` and `HF_DATASET_REPO` environment variables are set
|
| 9 |
+
- **Features**:
|
| 10 |
+
- Queue-based async processing (non-blocking)
|
| 11 |
+
- Automatic background worker thread
|
| 12 |
+
- Periodic HuggingFace uploads (every 5 minutes)
|
| 13 |
+
- Graceful shutdown with final flush
|
| 14 |
+
- Automatic dataset creation if needed
|
| 15 |
+
|
| 16 |
+
### 2. **Updated `app.py`**
|
| 17 |
+
- Added imports: `uuid4`, `log_chat`, `shutdown_logger`
|
| 18 |
+
- Added `_session_id` state variable (unique per session)
|
| 19 |
+
- Updated `sequential_thinking()`: Logs each thought with metadata
|
| 20 |
+
- Updated `reset_session()`: Generates new session ID when resetting
|
| 21 |
+
- Updated `__main__`: Calls `shutdown_logger()` on app exit for graceful cleanup
|
| 22 |
+
|
| 23 |
+
### 3. **New File: `LOGGING.md`**
|
| 24 |
+
- Complete documentation of logging features
|
| 25 |
+
- Configuration instructions
|
| 26 |
+
- Environment variables reference
|
| 27 |
+
- Data format examples
|
| 28 |
+
- Troubleshooting guide
|
| 29 |
+
|
| 30 |
+
## Usage
|
| 31 |
+
|
| 32 |
+
### No Configuration (Local Logging Only)
|
| 33 |
+
```bash
|
| 34 |
+
python app.py
|
| 35 |
+
```
|
| 36 |
+
- Logs all thoughts to `logs/chat_logs.jsonl`
|
| 37 |
+
- No external dependencies required
|
| 38 |
+
|
| 39 |
+
### With HuggingFace Upload
|
| 40 |
+
```bash
|
| 41 |
+
export HF_TOKEN=hf_xxxxxxxxxxxxx
|
| 42 |
+
export HF_DATASET_REPO=username/my-logs
|
| 43 |
+
python app.py
|
| 44 |
+
```
|
| 45 |
+
- Logs locally AND uploads to HuggingFace every 5 minutes
|
| 46 |
+
- Automatic dataset creation
|
| 47 |
+
|
| 48 |
+
## What Gets Logged
|
| 49 |
+
|
| 50 |
+
Each thought entry includes:
|
| 51 |
+
- **session_id**: Unique session identifier
|
| 52 |
+
- **model_name**: `"sequential-thinking"`
|
| 53 |
+
- **thought**: The actual thought text
|
| 54 |
+
- **thought_number**: Step number in sequence
|
| 55 |
+
- **total_thoughts**: Estimated total steps
|
| 56 |
+
- **metadata**: Revision/branch information
|
| 57 |
+
- **timestamp**: ISO 8601 timestamp
|
| 58 |
+
|
| 59 |
+
Example:
|
| 60 |
+
```json
|
| 61 |
+
{
|
| 62 |
+
"session_id": "abc123xyz...",
|
| 63 |
+
"model_name": "sequential-thinking",
|
| 64 |
+
"thought": "First, let me understand the problem...",
|
| 65 |
+
"thought_number": 1,
|
| 66 |
+
"total_thoughts": 5,
|
| 67 |
+
"metadata": {
|
| 68 |
+
"is_revision": false,
|
| 69 |
+
"branch_id": null,
|
| 70 |
+
"next_thought_needed": true
|
| 71 |
+
},
|
| 72 |
+
"timestamp": "2024-01-15T10:30:00.123456"
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## Key Features
|
| 77 |
+
|
| 78 |
+
✅ **Always on** - Local logging works without any configuration
|
| 79 |
+
✅ **Optional upload** - HuggingFace integration only if credentials set
|
| 80 |
+
✅ **Async processing** - Non-blocking, runs in background threads
|
| 81 |
+
✅ **Graceful shutdown** - Flushes all pending logs before exit
|
| 82 |
+
✅ **Session tracking** - Groups related thoughts by session ID
|
| 83 |
+
✅ **Error handling** - Continues working even if upload fails
|
| 84 |
+
|
| 85 |
+
## Files Modified/Created
|
| 86 |
+
|
| 87 |
+
- ✅ Created: `chat_logger.py` (140 lines)
|
| 88 |
+
- ✅ Created: `LOGGING.md` (documentation)
|
| 89 |
+
- ✅ Updated: `app.py` (added logging integration)
|
docs/LOGGING.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sequential Thinking with Chat Logging
|
| 2 |
+
|
| 3 |
+
This Gradio application now includes automatic chat logging for all thoughts and thinking processes.
|
| 4 |
+
|
| 5 |
+
## Chat Logging Features
|
| 6 |
+
|
| 7 |
+
### Local Logging (Always Enabled)
|
| 8 |
+
|
| 9 |
+
All thought records are automatically logged to a local JSONL file at `logs/chat_logs.jsonl`:
|
| 10 |
+
|
| 11 |
+
```json
|
| 12 |
+
{
|
| 13 |
+
"session_id": "abc123xyz789...",
|
| 14 |
+
"model_name": "sequential-thinking",
|
| 15 |
+
"thought": "Let me break down this problem step by step...",
|
| 16 |
+
"thought_number": 1,
|
| 17 |
+
"total_thoughts": 5,
|
| 18 |
+
"metadata": {
|
| 19 |
+
"is_revision": false,
|
| 20 |
+
"revises_thought": null,
|
| 21 |
+
"branch_from_thought": null,
|
| 22 |
+
"branch_id": null,
|
| 23 |
+
"needs_more_thoughts": false,
|
| 24 |
+
"next_thought_needed": true
|
| 25 |
+
},
|
| 26 |
+
"timestamp": "2024-01-15T10:30:00.123456"
|
| 27 |
+
}
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
#### Accessing Local Logs
|
| 31 |
+
|
| 32 |
+
Local logs are written asynchronously to `logs/chat_logs.jsonl`. Each line is a complete JSON object representing one thought entry.
|
| 33 |
+
|
| 34 |
+
### HuggingFace Dataset Upload (Optional)
|
| 35 |
+
|
| 36 |
+
If you have HuggingFace Hub credentials configured, logs are automatically uploaded periodically:
|
| 37 |
+
|
| 38 |
+
#### Configuration
|
| 39 |
+
|
| 40 |
+
Set these environment variables:
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
export HF_TOKEN=hf_xxxxxxxxxxxxx
|
| 44 |
+
export HF_DATASET_REPO=username/my-sequential-thinking-logs
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
#### Upload Behavior
|
| 48 |
+
|
| 49 |
+
- **Interval**: Every 5 minutes (configurable in `chat_logger.py`)
|
| 50 |
+
- **Format**: Timestamped files named `logs/chat_logs_YYYYMMDD_HHMMSS.jsonl`
|
| 51 |
+
- **Cleanup**: Local file is cleared after successful upload
|
| 52 |
+
- **Fallback**: If upload fails, logs remain locally and retry on next interval
|
| 53 |
+
|
| 54 |
+
#### Creating a HuggingFace Dataset
|
| 55 |
+
|
| 56 |
+
If the dataset doesn't exist, the logger creates it automatically on first upload (must be private).
|
| 57 |
+
|
| 58 |
+
## Running the App
|
| 59 |
+
|
| 60 |
+
### Standard Launch
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
python app.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
The app will:
|
| 67 |
+
1. Start the Gradio interface at `http://localhost:7860`
|
| 68 |
+
2. Begin logging all thoughts to `logs/chat_logs.jsonl`
|
| 69 |
+
3. Upload to HuggingFace (if configured) every 5 minutes
|
| 70 |
+
4. Flush remaining logs on shutdown
|
| 71 |
+
|
| 72 |
+
### Environment Variables
|
| 73 |
+
|
| 74 |
+
| Variable | Default | Description |
|
| 75 |
+
|----------|---------|-------------|
|
| 76 |
+
| `HF_TOKEN` | - | HuggingFace API token (optional) |
|
| 77 |
+
| `HF_DATASET_REPO` | - | HuggingFace dataset repo ID (optional) |
|
| 78 |
+
| `DISABLE_THOUGHT_LOGGING` | `false` | Suppress console output of thoughts |
|
| 79 |
+
|
| 80 |
+
### Disabling Console Output
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
DISABLE_THOUGHT_LOGGING=true python app.py
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Logs will still be saved locally; only console printing is suppressed.
|
| 87 |
+
|
| 88 |
+
## MCP Server
|
| 89 |
+
|
| 90 |
+
The app also exposes an MCP (Model Context Protocol) endpoint:
|
| 91 |
+
|
| 92 |
+
**SSE Endpoint**: `http://localhost:7860/gradio_api/mcp/sse`
|
| 93 |
+
|
| 94 |
+
Configure in your MCP client:
|
| 95 |
+
|
| 96 |
+
```json
|
| 97 |
+
{
|
| 98 |
+
"mcpServers": {
|
| 99 |
+
"sequential-thinking": {
|
| 100 |
+
"url": "http://localhost:7860/gradio_api/mcp/sse"
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## Log Data Structure
|
| 107 |
+
|
| 108 |
+
Each logged thought contains:
|
| 109 |
+
|
| 110 |
+
- **session_id**: Unique identifier for the current thinking session
|
| 111 |
+
- **model_name**: Always `"sequential-thinking"` for this app
|
| 112 |
+
- **thought**: The actual thought text
|
| 113 |
+
- **thought_number**: Current step number
|
| 114 |
+
- **total_thoughts**: Estimated total steps
|
| 115 |
+
- **metadata**: Additional context including revision/branch info
|
| 116 |
+
- **timestamp**: ISO 8601 timestamp
|
| 117 |
+
|
| 118 |
+
## Sessions
|
| 119 |
+
|
| 120 |
+
A new session ID is generated:
|
| 121 |
+
- When the app starts
|
| 122 |
+
- When "Reset Session" is clicked in the UI
|
| 123 |
+
|
| 124 |
+
All thoughts in a session share the same `session_id`, making it easy to group related thinking processes.
|
| 125 |
+
|
| 126 |
+
## Data Privacy
|
| 127 |
+
|
| 128 |
+
- Local logs are stored in `logs/chat_logs.jsonl` on your machine
|
| 129 |
+
- Only uploaded to HuggingFace if credentials are configured
|
| 130 |
+
- Uploaded datasets are set to private by default
|
| 131 |
+
- Logs are cleared from local storage after successful HuggingFace upload
|
| 132 |
+
|
| 133 |
+
## Troubleshooting
|
| 134 |
+
|
| 135 |
+
### Logs Not Appearing
|
| 136 |
+
|
| 137 |
+
1. Check that `logs/` directory exists: `ls logs/`
|
| 138 |
+
2. Verify JSONL file: `tail -f logs/chat_logs.jsonl`
|
| 139 |
+
3. Check console for any error messages
|
| 140 |
+
|
| 141 |
+
### HuggingFace Upload Issues
|
| 142 |
+
|
| 143 |
+
1. Verify `HF_TOKEN` is valid: `huggingface-cli whoami`
|
| 144 |
+
2. Ensure `HF_DATASET_REPO` follows format: `username/repo-name`
|
| 145 |
+
3. Check internet connection
|
| 146 |
+
4. Logs will retry on next interval; check for error messages in console
|
| 147 |
+
|
| 148 |
+
### Performance
|
| 149 |
+
|
| 150 |
+
Logging runs in background threads and has minimal impact on performance:
|
| 151 |
+
- Queue-based async processing
|
| 152 |
+
- Non-blocking JSONL writes
|
| 153 |
+
- Periodic batch uploads to HuggingFace
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio[mcp]==6.11.0
|
run.ps1
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Activate the virtual environment and launch the Gradio MCP app.
|
| 2 |
+
# The -X utf8 flag forces UTF-8 I/O so Gradio's emoji startup messages
|
| 3 |
+
# render correctly on Windows terminals.
|
| 4 |
+
|
| 5 |
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
| 6 |
+
& "$scriptDir\venv\Scripts\python.exe" -X utf8 -u "$scriptDir\app.py" @args
|
styles.css
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--color-grey-50: #f9fafb;
|
| 3 |
+
--banner-background: var(--secondary-400);
|
| 4 |
+
--banner-text-color: var(--primary-200);
|
| 5 |
+
--banner-background-dark: var(--secondary-800);
|
| 6 |
+
--banner-text-color-dark: var(--primary-100);
|
| 7 |
+
--banner-text-a-color: var(--primary-100);
|
| 8 |
+
--banner-text-a-color-dark: var(--secondary-100);
|
| 9 |
+
--banner-chrome-height: calc(16px + 43px);
|
| 10 |
+
|
| 11 |
+
/* Chrome height with no banner */
|
| 12 |
+
--chat-chrome-height-wide-no-banner: 320px;
|
| 13 |
+
--chat-chrome-height-narrow-no-banner: 450px;
|
| 14 |
+
|
| 15 |
+
/* Use these when we are not using a banner */
|
| 16 |
+
/*--chat-chrome-height-wide: var(--chat-chrome-height-wide-no-banner);*/
|
| 17 |
+
/*--chat-chrome-height-narrow: var(--chat-chrome-height-narrow-no-banner);*/
|
| 18 |
+
|
| 19 |
+
/* When we are using a banner, add the banner height to the chat chrome height */
|
| 20 |
+
--chat-chrome-height-wide: calc(var(--chat-chrome-height-wide-no-banner) + var(--banner-chrome-height));
|
| 21 |
+
--chat-chrome-height-narrow: calc(var(--chat-chrome-height-narrow-no-banner) + var(--banner-chrome-height));
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.banner-message {
|
| 25 |
+
background-color: var(--banner-background);
|
| 26 |
+
padding: 5px;
|
| 27 |
+
margin: 0;
|
| 28 |
+
border-radius: 5px;
|
| 29 |
+
border: none;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.banner-message-text {
|
| 33 |
+
font-size: 14px;
|
| 34 |
+
font-weight: bolder;
|
| 35 |
+
color: var(--banner-text-color) !important;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.banner-message-emoji {
|
| 39 |
+
margin-right: 3px;
|
| 40 |
+
font-size: 16px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.banner-message-text a {
|
| 44 |
+
color: var(--banner-text-a-color) !important;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
body.dark .banner-message {
|
| 48 |
+
background-color: var(--banner-background-dark) !important;
|
| 49 |
+
}
|
| 50 |
+
body.dark .gradio-container .contain .banner-message .banner-message-text {
|
| 51 |
+
color: var(--banner-text-color-dark) !important;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
body.dark .gradio-container .contain .banner-message .banner-message-text a {
|
| 55 |
+
color: var(--banner-text-a-color-dark) !important;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.toast-body {
|
| 59 |
+
background-color: var(--color-grey-50);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.html-container:has(.css-styles) {
|
| 63 |
+
padding: 0;
|
| 64 |
+
margin: 0;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.css-styles {
|
| 68 |
+
height: 0;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.model-message {
|
| 72 |
+
text-align: end;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.model-dropdown-container {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 10px;
|
| 79 |
+
padding: 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.user-input-container .multimodal-textbox{
|
| 83 |
+
border: none !important;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Match the height of the modified multimodal input box on the same row */
|
| 87 |
+
.control-button {
|
| 88 |
+
height: 51px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
button.cancel {
|
| 92 |
+
border: var(--button-border-width) solid var(--button-cancel-border-color);
|
| 93 |
+
background: var(--button-cancel-background-fill);
|
| 94 |
+
color: var(--button-cancel-text-color);
|
| 95 |
+
box-shadow: var(--button-cancel-shadow);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
button.cancel:hover, .cancel[disabled] {
|
| 99 |
+
background: var(--button-cancel-background-fill-hover);
|
| 100 |
+
color: var(--button-cancel-text-color-hover);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.opt-out-message {
|
| 104 |
+
top: 8px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.opt-out-message .html-container, .opt-out-checkbox label {
|
| 108 |
+
font-size: 14px !important;
|
| 109 |
+
padding: 0 !important;
|
| 110 |
+
margin: 0 !important;
|
| 111 |
+
color: var(--neutral-400) !important;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
div.block.chatbot {
|
| 115 |
+
height: calc(100svh - var(--chat-chrome-height-wide)) !important;
|
| 116 |
+
max-height: 900px !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
div.no-padding {
|
| 120 |
+
padding: 0 !important;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
@media (max-width: 1280px) {
|
| 124 |
+
div.block.chatbot {
|
| 125 |
+
height: calc(100svh - var(--chat-chrome-height-wide)) !important;
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
@media (max-width: 1024px) {
|
| 130 |
+
.responsive-row {
|
| 131 |
+
flex-direction: column;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.model-message {
|
| 135 |
+
text-align: start;
|
| 136 |
+
font-size: 10px !important;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.model-dropdown-container {
|
| 140 |
+
flex-direction: column;
|
| 141 |
+
align-items: flex-start;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
div.block.chatbot {
|
| 145 |
+
height: calc(100svh - var(--chat-chrome-height-narrow)) !important;
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
@media (max-width: 400px) {
|
| 150 |
+
.responsive-row {
|
| 151 |
+
flex-direction: column;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.model-message {
|
| 155 |
+
text-align: start;
|
| 156 |
+
font-size: 10px !important;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.model-dropdown-container {
|
| 160 |
+
flex-direction: column;
|
| 161 |
+
align-items: flex-start;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
div.block.chatbot {
|
| 165 |
+
max-height: 360px !important;
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
@media (max-height: 932px) {
|
| 170 |
+
.chatbot {
|
| 171 |
+
max-height: 500px !important;
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
@media (max-height: 1280px) {
|
| 176 |
+
div.block.chatbot {
|
| 177 |
+
max-height: 800px !important;
|
| 178 |
+
}
|
| 179 |
+
}
|
theme.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Soft Professional Tech Theme for Sequential Thinking MCP App
|
| 3 |
+
Clean, modern aesthetic with teal-gray accents, soft gradients, and professional typography.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Iterable
|
| 7 |
+
from gradio.themes import Soft
|
| 8 |
+
from gradio.themes.utils import colors, fonts, sizes
|
| 9 |
+
|
| 10 |
+
# Primary accent color - Teal Gray
|
| 11 |
+
# A sophisticated blue-teal that works well in both light and dark modes
|
| 12 |
+
colors.teal_gray = colors.Color(
|
| 13 |
+
name="teal_gray",
|
| 14 |
+
c50="#e8f1f4", # Lightest - backgrounds
|
| 15 |
+
c100="#cddde3", # Light accents
|
| 16 |
+
c200="#a8c3cf", # Subtle highlights
|
| 17 |
+
c300="#7da6b8", # Secondary text
|
| 18 |
+
c400="#588aa2", # Primary buttons
|
| 19 |
+
c500="#3d6e87", # Base accent color
|
| 20 |
+
c600="#335b70", # Hover states
|
| 21 |
+
c700="#2b495a", # Dark mode accents
|
| 22 |
+
c800="#2c5364", # Dark backgrounds
|
| 23 |
+
c900="#233f4b", # Darker backgrounds
|
| 24 |
+
c950="#1b323c", # Darkest
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Cancel/destructive action color - Muted Red Gray
|
| 28 |
+
# Less aggressive than pure red, better for professional UIs
|
| 29 |
+
colors.red_gray = colors.Color(
|
| 30 |
+
name="red_gray",
|
| 31 |
+
c50="#f7eded",
|
| 32 |
+
c100="#f5dcdc",
|
| 33 |
+
c200="#efb4b4",
|
| 34 |
+
c300="#e78f8f",
|
| 35 |
+
c400="#d96a6a", # Cancel button background
|
| 36 |
+
c500="#c65353", # Cancel button hover
|
| 37 |
+
c600="#b24444",
|
| 38 |
+
c700="#8f3434", # Dark mode cancel
|
| 39 |
+
c800="#732d2d",
|
| 40 |
+
c900="#5f2626",
|
| 41 |
+
c950="#4d2020",
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class SoftProfessionalTheme(Soft):
|
| 46 |
+
"""
|
| 47 |
+
Soft Professional Tech Theme
|
| 48 |
+
|
| 49 |
+
A clean, modern theme with teal-gray accents, soft gradients,
|
| 50 |
+
and professional typography. Suitable for chat apps, dashboards,
|
| 51 |
+
and any professional-grade Gradio application.
|
| 52 |
+
"""
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
*,
|
| 56 |
+
primary_hue: colors.Color | str = colors.gray,
|
| 57 |
+
secondary_hue: colors.Color | str = colors.teal_gray,
|
| 58 |
+
neutral_hue: colors.Color | str = colors.slate,
|
| 59 |
+
text_size: sizes.Size | str = sizes.text_md,
|
| 60 |
+
font: fonts.Font | str | Iterable[fonts.Font | str] = (
|
| 61 |
+
fonts.GoogleFont("Inconsolata"),
|
| 62 |
+
"Arial",
|
| 63 |
+
"sans-serif",
|
| 64 |
+
),
|
| 65 |
+
font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
|
| 66 |
+
fonts.GoogleFont("IBM Plex Mono"),
|
| 67 |
+
"ui-monospace",
|
| 68 |
+
"monospace",
|
| 69 |
+
),
|
| 70 |
+
):
|
| 71 |
+
super().__init__(
|
| 72 |
+
primary_hue=primary_hue,
|
| 73 |
+
secondary_hue=secondary_hue,
|
| 74 |
+
neutral_hue=neutral_hue,
|
| 75 |
+
text_size=text_size,
|
| 76 |
+
font=font,
|
| 77 |
+
font_mono=font_mono,
|
| 78 |
+
)
|
| 79 |
+
super().set(
|
| 80 |
+
# === BACKGROUNDS ===
|
| 81 |
+
# Soft diagonal gradient for body - creates depth without distraction
|
| 82 |
+
body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
|
| 83 |
+
body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
|
| 84 |
+
background_fill_primary="*primary_50",
|
| 85 |
+
background_fill_primary_dark="*primary_900",
|
| 86 |
+
|
| 87 |
+
# === PRIMARY BUTTONS ===
|
| 88 |
+
# Teal-gray gradient buttons with proper hover states
|
| 89 |
+
button_primary_text_color="white",
|
| 90 |
+
button_primary_text_color_hover="black",
|
| 91 |
+
button_primary_background_fill="linear-gradient(90deg, *secondary_400, *secondary_400)",
|
| 92 |
+
button_primary_background_fill_hover="linear-gradient(90deg, *secondary_300, *secondary_300)",
|
| 93 |
+
button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_800)",
|
| 94 |
+
button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_500)",
|
| 95 |
+
button_primary_shadow="*shadow_drop_lg",
|
| 96 |
+
|
| 97 |
+
# === SECONDARY BUTTONS ===
|
| 98 |
+
# Subtle gray gradient for secondary actions
|
| 99 |
+
button_secondary_text_color="black",
|
| 100 |
+
button_secondary_text_color_hover="white",
|
| 101 |
+
button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
|
| 102 |
+
button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
|
| 103 |
+
button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
|
| 104 |
+
button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
|
| 105 |
+
|
| 106 |
+
# === CANCEL/STOP BUTTONS ===
|
| 107 |
+
# Muted red-gray for less aggressive cancel actions
|
| 108 |
+
button_cancel_background_fill=f"linear-gradient(90deg, {colors.red_gray.c400}, {colors.red_gray.c500})",
|
| 109 |
+
button_cancel_background_fill_dark=f"linear-gradient(90deg, {colors.red_gray.c700}, {colors.red_gray.c800})",
|
| 110 |
+
button_cancel_background_fill_hover=f"linear-gradient(90deg, {colors.red_gray.c500}, {colors.red_gray.c600})",
|
| 111 |
+
button_cancel_background_fill_hover_dark=f"linear-gradient(90deg, {colors.red_gray.c800}, {colors.red_gray.c900})",
|
| 112 |
+
button_cancel_text_color="white",
|
| 113 |
+
button_cancel_text_color_dark="white",
|
| 114 |
+
button_cancel_text_color_hover="white",
|
| 115 |
+
button_cancel_text_color_hover_dark="white",
|
| 116 |
+
|
| 117 |
+
# === SLIDERS ===
|
| 118 |
+
slider_color="*secondary_300",
|
| 119 |
+
slider_color_dark="*secondary_600",
|
| 120 |
+
|
| 121 |
+
# === BLOCKS & CONTAINERS ===
|
| 122 |
+
block_title_text_weight="600",
|
| 123 |
+
block_border_width="3px",
|
| 124 |
+
block_shadow="*shadow_drop_lg",
|
| 125 |
+
block_label_background_fill="*primary_200",
|
| 126 |
+
|
| 127 |
+
# === BUTTONS GENERAL ===
|
| 128 |
+
button_large_padding="11px",
|
| 129 |
+
|
| 130 |
+
# === ACCENTS ===
|
| 131 |
+
color_accent_soft="*primary_100",
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# Export theme instance for easy import
|
| 136 |
+
soft_professional_theme = SoftProfessionalTheme()
|