├── .env.template ├── .gitignore ├── .python-version ├── README.md ├── __pycache__ └── main.cpython-312.pyc ├── adventai ├── components │ ├── components.js │ ├── gift-list.js │ └── preview-tiles.js ├── favicon.png ├── index.html ├── og │ ├── adventai.png │ ├── advice-from-an-expert.png │ ├── career-coach.png │ ├── chef-boss.png │ ├── deal-finder.png │ ├── job-digest.png │ ├── mealplanner.png │ ├── news-digest.png │ ├── one-fact-a-day.png │ ├── random-pet-fact.png │ ├── real-estate-bot.png │ ├── stonks-simulator.png │ ├── wwwed.png │ └── x-digest.png ├── script.js └── styles.css ├── api ├── __init__.py └── chat │ ├── __init__.py │ └── __pycache__ │ ├── __init__.cpython-312.pyc │ └── post.cpython-312.pyc ├── helpers ├── __init__.py └── experimental.py ├── hypercorn_config.py ├── llms ├── __init__.py └── __pycache__ │ └── __init__.cpython-312.pyc ├── main.py ├── poetry.lock ├── pyproject.toml └── static ├── components ├── action-box.js ├── chat-history-container.js ├── components.js ├── main-app.js ├── resizable-textarea.js ├── success-message.js └── toolhouse-icon.js ├── css ├── components │ └── resizable-textarea.css └── style.css ├── domo.js ├── fonts ├── GT-Cinetype-Mono.woff ├── WebPlus_IBM_VGA_8x16-2x.woff └── WebPlus_IBM_VGA_8x16.woff ├── helpers └── stream.js └── index.html /.env.template: -------------------------------------------------------------------------------- 1 | ENVIRONMENT=development 2 | TOOLHOUSE_API_KEY="Get your API Key at https://app.toolhouse.ai/api-keys" 3 | GROQCLOUD_API_KEY="Get your Groq API Key at https://console.groq.com" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | .env 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 111 | .pdm.toml 112 | .pdm-python 113 | .pdm-build/ 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | .DS_Store 165 | *.pem -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.7 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeepSeek R1 + Toolhouse Function Calling Proof Of Concept 2 | 3 | This repo contains a proof-of-concept app that demonstrates how to enable function calling on DeepSeek using [Toolhouse](https://app.toolhouse.ai) as the function calling infrastructure. 4 | 5 | **[Sign up for Toolhouse](https://toolhouse.ai) (it's free)** 6 | 7 | ![groq](https://github.com/user-attachments/assets/1ab7d578-6048-424d-9949-c7af160fdf32) 8 | 9 | 10 | **Note:** this app is experimental and not intended for production use cases. Its sole purpose is to demonstrate DeepSeek's ability to leverage function calling using common prompt techniques. 11 | 12 | This demo uses `deepseek-r1-distill-llama` as provided by [Groq](https://console.groq.com?utm_source=toolhouse). 13 | 14 | ## How does it work? 15 | 16 | We use a common system prompt to instruct DeepSeek that it has tools at its disposal. We list the tools and we give DeepSeek precise instructions on how to call a tool: 17 | 18 | ``` 19 | In this environment you will have access to a set of tools you can use to help answer the user's question. 20 | 21 | You can call them like this: 22 | 23 | { 24 | "function_calls": [ 25 | { 26 | "tool_name": "$TOOL_NAME", 27 | "parameters": {"$PARAMETER_NAME": "$PARAMETER_VALUE"}, 28 | } 29 | ] 30 | } 31 | 32 | 33 | Here are the tools available: 34 | {LIST_OF_TOOLHOUSE_TOOLS} 35 | 36 | Make sure to call one tool at a time. Make sure to respect the parameter type, ensuring to wrap string values in quotes, and leaving numeric values unwrapped. Feel free to use as many tools as you need. 37 | 38 | If you can't find the right tool for your purpose, say "I'm sorry, I don't have the right tools in my toolbelt to answer that question". 39 | 40 | The user will give you a response from the tool in a message that begins with "Response from tool:". When you see that string, treat it as the tool response. 41 | ``` 42 | 43 | Because there isn't a specific `tool` role, we instructed the model to treat specific user messages as user tools results. 44 | 45 | On each completion call, a function inspects the contents of the `assistant` message and looks for a valid function call. When detected, the function parses the call (including its arguments) and passes it to Toolhouse to run it. Toolhouse runs the tool and returns the result back to the code. From that point, the code gets the response from Toolhouse and formats it as a tool response by prepending `Response from tool:` to the tool response. 46 | 47 | ## Is that it? I heard DeepSeek R1 is bad at function calling 48 | 49 | While our tests confirm DeepSeek R1's function calling is not on par with leading models, Groq's version performs surprisingly well at completing simple tasks. Here are some findings from our early limited testing: 50 | 51 | - The model shows reasonable performance in selecting the right tools. 52 | - The model had a ~84% rate in selecting the expected tool for each task at hand. 53 | - The model tries to avoid selecting tools. In other words, DeepSeek may not select a tool when instead it should select it. We believe this can be mitigated by adding specific prompt directives, but we haven't explored this further. 54 | - The model's thinking step leads it to hallucinate. For example, while using the `current_time` tool, we discovered that the model tricked itself into thinking that it made a tool call, and generated a tool response with a wrong timestamp answer. This happens consistently. 55 | - The model does not exhibit agentic capabilities that allows it to perform multiple-turn tool calls. 56 | - The model generates structured tool calls reliably in the format we specified. 57 | - We have yet to encounter issues like parameter hallucination, which affected leading models and smaller-parameter models. 58 | 59 | 60 | ## How to run on your environment 61 | 62 | ### Prerequisites 63 | 64 | - A Toolhouse API key. You can get a free API key plus 500 free execution every month when you [sign up for Toolhouse](https://app.toolhouse.ai). 65 | - A Groq API Key. You can get a free API key on their [developer console](https://console.groq.com?utm_source=toolhouse). 66 | - Python 3.12 67 | - Poetry 68 | 69 | #### Setup 70 | 71 | ```bash 72 | poetry install 73 | ``` 74 | 75 | #### Run 76 | 77 | To run on http://0.0.0.0:8000/app/deepseek 78 | 79 | ```bash 80 | hypercorn main:app --bind 0.0.0.0:8000 81 | ``` 82 | 83 | You can use `watchexec` to monitor changes and reload changes automatically. 84 | 85 | ```bash 86 | watchexec -r -e py "hypercorn main:app --bind 0.0.0.0:8000" 87 | ``` 88 | 89 | To reload changes you made to the frontend, simply refresh your browser. 90 | 91 | ### Development 92 | 93 | Because your function calling infrastructure and code are hosted on Toolhouse, all apps are actually just a collection of prompts and some business logic. 94 | 95 | Each app has this: 96 | 97 | - **main.py** is the main entry point. It sets up the API routes and serves static content. 98 | - **api** contains the backend endpoints: 99 | - **api/chat** streams responses to the LLM you choose 100 | - **static** contains the frontend 101 | -------------------------------------------------------------------------------- /__pycache__/main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/__pycache__/main.cpython-312.pyc -------------------------------------------------------------------------------- /adventai/components/components.js: -------------------------------------------------------------------------------- 1 | export * from "/app/components/icons/index.js"; 2 | export { GenericIcon } from "/app/components/generic-icon.js"; 3 | export { PreviewTiles } from "./preview-tiles.js"; 4 | export { GiftList } from "./gift-list.js"; -------------------------------------------------------------------------------- /adventai/components/gift-list.js: -------------------------------------------------------------------------------- 1 | import Domo, {html} from '/app/domo.js'; 2 | 3 | export class GiftList extends Domo { 4 | render() { 5 | if (window.gifts.length === 0) { 6 | this.style.display = 'none'; 7 | return null; 8 | } 9 | 10 | return html` 11 |

Unwrap Your Apps!

12 | `; 13 | } 14 | } -------------------------------------------------------------------------------- /adventai/components/preview-tiles.js: -------------------------------------------------------------------------------- 1 | import Domo, {html} from '/app/domo.js'; 2 | 3 | export class PreviewTiles extends Domo { 4 | tiltTile({currentTarget}) { 5 | const randomAngle = (Math.random() * 4 - 2).toFixed(2); // Random angle between -2 and 2 degrees 6 | currentTarget.style.transform = `scale(110%) translateY(-10px) rotate(${randomAngle}deg)`; 7 | } 8 | 9 | tiltTileBack({currentTarget}) { 10 | currentTarget.style.transform = 'translateY(0) rotate(0deg)'; 11 | } 12 | 13 | render() { 14 | if (window.gifts.length === 0) { 15 | this.style.display = 'none'; 16 | return null; 17 | } 18 | 19 | return window.gifts.map(({app_name, title}, i) => 20 | html` 21 | 27 | 28 |
29 |
Day ${i + 1}
30 |

${title}

31 |
32 |
`) 33 | } 34 | } -------------------------------------------------------------------------------- /adventai/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/favicon.png -------------------------------------------------------------------------------- /adventai/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AdventAI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

⭐ AdventAI ⭐

22 |

12 days of Christmas, AI style!

23 |

24 | Unwrap 1️⃣2️⃣ Apps you can build with Toolhouse. Use them, and make them yours! 25 |

26 | 27 |
28 |
29 | 35 | 38 |
39 |
40 | 41 | 42 | 43 |
44 | Brought to you by Toolhouse. 🛠️ 45 |
46 |
47 | 48 | 49 | 55 | 56 | -------------------------------------------------------------------------------- /adventai/og/adventai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/adventai.png -------------------------------------------------------------------------------- /adventai/og/advice-from-an-expert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/advice-from-an-expert.png -------------------------------------------------------------------------------- /adventai/og/career-coach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/career-coach.png -------------------------------------------------------------------------------- /adventai/og/chef-boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/chef-boss.png -------------------------------------------------------------------------------- /adventai/og/deal-finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/deal-finder.png -------------------------------------------------------------------------------- /adventai/og/job-digest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/job-digest.png -------------------------------------------------------------------------------- /adventai/og/mealplanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/mealplanner.png -------------------------------------------------------------------------------- /adventai/og/news-digest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/news-digest.png -------------------------------------------------------------------------------- /adventai/og/one-fact-a-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/one-fact-a-day.png -------------------------------------------------------------------------------- /adventai/og/random-pet-fact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/random-pet-fact.png -------------------------------------------------------------------------------- /adventai/og/real-estate-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/real-estate-bot.png -------------------------------------------------------------------------------- /adventai/og/stonks-simulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/stonks-simulator.png -------------------------------------------------------------------------------- /adventai/og/wwwed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/wwwed.png -------------------------------------------------------------------------------- /adventai/og/x-digest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/adventai/og/x-digest.png -------------------------------------------------------------------------------- /adventai/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const emailInput = document.getElementById('email-input'); 3 | const submitButton = document.getElementById('submit-button'); 4 | const emailForm = document.getElementById('email-form'); 5 | const countdownText = document.getElementById('countdown-text'); 6 | const christmasBg = document.querySelector('.christmas-background'); 7 | 8 | const NUM_SNOWFLAKES = 50; 9 | const NUM_CANDYCANES = 20; 10 | 11 | function validateEmail(email) { 12 | const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 13 | return re.test(String(email).toLowerCase()); 14 | } 15 | 16 | function createFallingItems(num, emoji, rotate = false) { 17 | for (let i = 0; i < num; i++) { 18 | const item = document.createElement('div'); 19 | item.textContent = emoji; 20 | item.classList.add(rotate ? 'candycane' : 'snowflake'); 21 | 22 | const left = Math.random() * 100; 23 | const animationDuration = 5 + Math.random() * 10; 24 | const delay = Math.random() * 5; 25 | 26 | item.style.top = '-40px'; 27 | item.style.left = `${left}%`; 28 | item.style.animationDuration = `${animationDuration}s`; 29 | item.style.animationDelay = `${delay}s`; 30 | 31 | if (rotate) { 32 | item.style.transform = 'rotate(45deg)'; 33 | } 34 | 35 | christmasBg.appendChild(item); 36 | } 37 | } 38 | 39 | // Create falling items 40 | createFallingItems(NUM_SNOWFLAKES, '❄️'); 41 | createFallingItems(NUM_CANDYCANES, '🎁', true); 42 | 43 | // Email validation 44 | emailInput.addEventListener('input', (e) => { 45 | const isValid = validateEmail(e.target.value); 46 | submitButton.disabled = !isValid; 47 | }); 48 | 49 | // Form submission 50 | emailForm.addEventListener('submit', async (e) => { 51 | e.preventDefault(); 52 | const email = emailInput.value; 53 | 54 | if (validateEmail(email)) { 55 | try { 56 | const url = new URL('https://hooks.zapier.com/hooks/catch/19666829/2izmzdp/'); 57 | url.searchParams.set('email', email); 58 | 59 | await fetch(url); 60 | 61 | localStorage.setItem('subscribed', 'true'); 62 | updateUI(); 63 | } catch (error) { 64 | console.error('Could not subscribe user:', error); 65 | } 66 | } 67 | }); 68 | 69 | function updateUI() { 70 | const isSubscribed = localStorage.getItem('subscribed') === 'true'; 71 | const emailSection = document.getElementById('email-section'); 72 | const subscribedMessage = document.createElement('p'); 73 | 74 | if (isSubscribed) { 75 | subscribedMessage.textContent = 'Check your inbox! You will receive a new AI gift every day. 🎉'; 76 | emailSection.innerHTML = ''; 77 | emailSection.appendChild(subscribedMessage); 78 | } 79 | } 80 | 81 | updateUI(); 82 | }); -------------------------------------------------------------------------------- /adventai/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Rethink+Sans:ital,wght@0,400..800;1,400..800&display=swap'); 2 | 3 | body, html { 4 | margin: 0; 5 | padding: 0; 6 | font-family: "Rethink Sans", sans-serif; 7 | height: 100%; 8 | background-color: #065f46; 9 | color: white; 10 | overflow-x: hidden; 11 | } 12 | 13 | input, button { 14 | font-family: "Rethink Sans", sans-serif; 15 | } 16 | 17 | .christmas-background { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | pointer-events: none; 24 | z-index: 1; 25 | overflow: hidden; 26 | } 27 | 28 | .landing-container { 29 | position: relative; 30 | z-index: 10; 31 | display: flex; 32 | flex-direction: column; 33 | align-items: center; 34 | justify-content: center; 35 | min-height: 100vh; 36 | text-align: center; 37 | padding: 0 20px; 38 | } 39 | 40 | h1 { 41 | font-size: 3rem; 42 | margin-bottom: 1rem; 43 | } 44 | 45 | .subtitle { 46 | font-size: 1.5rem; 47 | font-weight: bold; 48 | margin-bottom: 1rem; 49 | } 50 | 51 | .description { 52 | font-size: 1.25rem; 53 | max-width: 800px; 54 | margin-bottom: 2rem; 55 | } 56 | 57 | #email-section { 58 | width: 100%; 59 | max-width: 400px; 60 | } 61 | 62 | #email-form { 63 | display: flex; 64 | flex-direction: column; 65 | gap: 1rem; 66 | } 67 | 68 | #email-input { 69 | padding: 10px; 70 | border: 1px solid #ccc; 71 | border-radius: 5px; 72 | font-size: 1rem; 73 | } 74 | 75 | #submit-button { 76 | background-color: #dc2626; 77 | color: white; 78 | border: none; 79 | padding: 10px; 80 | border-radius: 20px; 81 | font-size: 1rem; 82 | cursor: pointer; 83 | transition: background-color 0.3s; 84 | } 85 | 86 | #submit-button:disabled { 87 | background-color: #888; 88 | cursor: not-allowed; 89 | } 90 | 91 | footer { 92 | margin-top: 2rem; 93 | font-size: 0.9rem; 94 | } 95 | 96 | footer a { 97 | color: white; 98 | } 99 | 100 | .snowflake, .candycane { 101 | position: absolute; 102 | top: -20px; 103 | font-size: 1.5rem; 104 | animation: fall linear infinite; 105 | pointer-events: none; 106 | } 107 | 108 | @keyframes fall { 109 | to { 110 | transform: translateY(100vh) rotate(360deg); 111 | } 112 | } 113 | 114 | preview-tiles { 115 | display: flex; 116 | flex-wrap: wrap; 117 | justify-content: center; 118 | gap: 2rem; 119 | padding: 2rem; 120 | background-color: rgba(255, 255, 255, 0.1); 121 | border-radius: 15px; 122 | margin: 2rem auto; 123 | max-width: 1200px; 124 | } 125 | 126 | .preview-tile { 127 | flex: 0 1 calc(50% - 2rem); 128 | max-width: 400px; 129 | min-width: 250px; 130 | aspect-ratio: 16/9; 131 | color: #fff; 132 | background-color: rgba(255, 255, 255, 0.2); 133 | border-radius: 15px; 134 | overflow: hidden; 135 | cursor: pointer; 136 | transition: all 0.3s ease; 137 | position: relative; 138 | box-shadow: 0 4px 6px rgba(0,0,0,0.1); 139 | } 140 | 141 | @media (max-width: 768px) { 142 | #preview-tiles { 143 | flex-direction: column; 144 | align-items: center; 145 | } 146 | 147 | .preview-tile { 148 | flex: 0 1 100%; 149 | width: 90%; 150 | max-width: 500px; 151 | } 152 | } 153 | 154 | .preview-tile img { 155 | width: 100%; 156 | height: 100%; 157 | object-fit: cover; 158 | } 159 | 160 | .preview-tile-overlay { 161 | position: absolute; 162 | bottom: 0; 163 | left: 0; 164 | right: 0; 165 | background-color: rgba(6, 95, 70, 0.9); 166 | color: white; 167 | padding: 1rem; 168 | display: flex; 169 | justify-content: space-between; 170 | align-items: center; 171 | } 172 | 173 | .preview-tile-overlay h3 { 174 | margin: 0; 175 | font-size: 1.2rem; 176 | } 177 | 178 | .preview-tile-overlay .day-tag { 179 | background-color: #dc2626; 180 | padding: 0.25rem 0.5rem; 181 | border-radius: 10px; 182 | font-size: 0.8rem; 183 | } -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/api/__init__.py -------------------------------------------------------------------------------- /api/chat/__init__.py: -------------------------------------------------------------------------------- 1 | from starlette.requests import Request 2 | from starlette.responses import StreamingResponse 3 | from llms import generate_stream 4 | from helpers import format_user_id, get_app_name, read_config 5 | import dotenv 6 | import json 7 | 8 | dotenv.load_dotenv() 9 | 10 | 11 | async def yield_error(text): 12 | yield text 13 | 14 | 15 | async def post(request: Request): 16 | try: 17 | body = await request.body() 18 | data = json.loads(body) 19 | except json.JSONDecodeError: 20 | return StreamingResponse( 21 | yield_error("JSON Decode Error"), media_type="text/plain", status_code=400 22 | ) 23 | 24 | name = "Simplechat" 25 | model = "deepseek-r1-distill-llama-70b" 26 | 27 | if not model: 28 | return StreamingResponse( 29 | yield_error("Missing required parameter: model"), 30 | media_type="text/plain", 31 | status_code=400, 32 | ) 33 | 34 | return StreamingResponse( 35 | generate_stream( 36 | messages=data.get("messages"), 37 | model=model, 38 | bundle=data.get("bundle", "default"), 39 | email=format_user_id(name, data.get("email")), 40 | ), 41 | media_type="text/plain", 42 | ) 43 | -------------------------------------------------------------------------------- /api/chat/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/api/chat/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /api/chat/__pycache__/post.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/api/chat/__pycache__/post.cpython-312.pyc -------------------------------------------------------------------------------- /helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from starlette.requests import Request 2 | from urllib.parse import urlparse 3 | import tomllib 4 | 5 | 6 | def read_config(file: str) -> dict | None: 7 | try: 8 | with open(file, "rb") as f: 9 | return tomllib.load(f) 10 | except FileNotFoundError as e: 11 | return None 12 | except tomllib.TOMLDecodeError as e: 13 | return None 14 | except Exception as e: 15 | return None 16 | 17 | 18 | def get_app_name(request: Request): 19 | referer = request.headers.get("referer") 20 | parsed_url = urlparse(referer) 21 | path_segments = parsed_url.path.strip("/").split("/") 22 | return path_segments[-1] if path_segments else None 23 | 24 | 25 | def format_user_id(appname: str | None, user: str | None) -> str | None: 26 | if appname and user: 27 | return f"{appname}-{user}" 28 | else: 29 | return None 30 | -------------------------------------------------------------------------------- /helpers/experimental.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import os 4 | 5 | 6 | def system_prompt(tools): 7 | json_sample_call = { 8 | "function_calls": [ 9 | { 10 | "tool_name": "$TOOL_NAME", 11 | "parameters": {"$PARAMETER_NAME": "$PARAMETER_VALUE"}, 12 | } 13 | ] 14 | } 15 | 16 | return f"""In this environment you will have access to a set of tools you can use to help answer the user's question. 17 | 18 | You can call them like this: 19 | 20 | {json.dumps(json_sample_call)} 21 | 22 | 23 | Here are the tools available: 24 | {json.dumps(tools)} 25 | 26 | Make sure to call one tool at a time. Make sure to respect the parameter type, ensuring to wrap string values in quotes, and leaving numeric values unwrapped. Feel free to use as many tools as you need. 27 | 28 | If you can't find the right tool for your purpose, say "I'm sorry, I don't have the right tools in my toolbelt to answer that question". 29 | 30 | The user will give you a response from the tool in a message that begins with "Response from tool:". When you see that string, treat it as the tool response. 31 | """ 32 | 33 | 34 | def run_tool(tool): 35 | req = { 36 | "metadata": {"toolhouse_id": "default", "toolhouse_timezone": 0}, 37 | "provider": "anthropic", 38 | "content": { 39 | "id": "deepseek_th_1337", 40 | "input": tool.get("parameters"), 41 | "name": tool.get("tool_name"), 42 | "type": "tool_use", 43 | }, 44 | "bundle": "default", 45 | } 46 | print(json.dumps(req)) 47 | response = requests.post( 48 | "https://api.toolhouse.ai/v1/run_tools", 49 | json=req, 50 | headers={ 51 | "Authorization": f"Bearer {os.environ.get('TOOLHOUSE_API_KEY')}", 52 | "Accept": "application/json", 53 | "Content-Type": "application/json", 54 | }, 55 | ) 56 | 57 | response.raise_for_status() 58 | return response.json() 59 | 60 | 61 | def find_tool_use(response): 62 | index = response.find("") 63 | stop = response.find("") 64 | if index < 0: 65 | return None 66 | 67 | start = index + len("") 68 | out = response[start:stop].strip() 69 | try: 70 | j = json.loads(out) 71 | 72 | if j.get("function_calls"): 73 | return j.get("function_calls")[0] 74 | except: 75 | return None 76 | -------------------------------------------------------------------------------- /hypercorn_config.py: -------------------------------------------------------------------------------- 1 | from hypercorn.config import Config 2 | 3 | config = Config() 4 | config.bind = ["0.0.0.0:8000"] 5 | config.alpn_protocols = ["h2"] 6 | config.certfile = "cert.pem" 7 | config.keyfile = "key.pem" 8 | config.ssl = "TLS" 9 | -------------------------------------------------------------------------------- /llms/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import traceback 4 | from openai import OpenAI 5 | from anthropic import Anthropic 6 | from toolhouse import Toolhouse, ToolhouseStreamStorage, stream_to_chat_completion 7 | from helpers.experimental import system_prompt, run_tool, find_tool_use 8 | 9 | models = { 10 | "DeepSeek R1": { 11 | "provider": "openai", 12 | "host": "groq", 13 | "model": "deepseek-r1-distill-llama-70b", 14 | }, 15 | } 16 | 17 | 18 | def get_model(model: str): 19 | for label in models.keys(): 20 | entry = models[label] 21 | if entry.get("model") == model: 22 | return entry 23 | 24 | return None 25 | 26 | 27 | class LLMContextManager(object): 28 | def __init__(self, sdk): 29 | self.sdk = sdk 30 | 31 | def __enter__(self): 32 | return self.sdk 33 | 34 | def __exit__(self, *args): 35 | pass 36 | 37 | 38 | def select_llm(host, **kwargs): 39 | if host == "groq": 40 | return call_groq(**kwargs) 41 | elif host == "together": 42 | return call_together(**kwargs) 43 | elif host == "openai": 44 | return call_openai(**kwargs) 45 | elif host == "anthropic": 46 | return call_anthropic(**kwargs) 47 | else: 48 | raise Exception(f"Invalid LLM host: {host}") 49 | 50 | 51 | def llm_call(**kwargs): 52 | model = kwargs.get("model") 53 | provider = get_model(model).get("host") 54 | if not kwargs.get("stream", False): 55 | return LLMContextManager(select_llm(provider, **kwargs)) 56 | else: 57 | return select_llm(provider, **kwargs) 58 | 59 | 60 | def call_openai(**kwargs): 61 | client = OpenAI() 62 | args = kwargs.copy() 63 | 64 | if not next((m["role"] == "system" for m in args["messages"]), None): 65 | args["messages"] = [{"role": "system", "content": system_prompt}] + args[ 66 | "messages" 67 | ] 68 | 69 | if args.get("system"): 70 | args["messages"] = [{"role": "system", "content": args.get("system")}] + args[ 71 | "messages" 72 | ] 73 | del args["system"] 74 | 75 | return client.chat.completions.create(**args) 76 | 77 | 78 | def call_anthropic(**kwargs): 79 | client = Anthropic() 80 | args = kwargs.copy() 81 | args["system"] = system_prompt 82 | 83 | if kwargs.get("tools") is None: 84 | del args["tools"] 85 | 86 | if kwargs.get("stream"): 87 | del args["stream"] 88 | return client.messages.stream(**args) 89 | else: 90 | return client.messages.create(**args) 91 | 92 | 93 | def call_groq(**kwargs): 94 | args = kwargs.copy() 95 | client = OpenAI( 96 | api_key=os.environ.get("GROQCLOUD_API_KEY"), 97 | base_url="https://api.groq.com/openai/v1", 98 | ) 99 | 100 | args = kwargs.copy() 101 | 102 | if not next((m["role"] == "system" for m in args["messages"]), None): 103 | args["messages"] = [{"role": "system", "content": args.get("system")}] + args[ 104 | "messages" 105 | ] 106 | 107 | if args.get("system"): 108 | args["messages"] = [{"role": "system", "content": args.get("system")}] + args[ 109 | "messages" 110 | ] 111 | del args["system"] 112 | 113 | return client.chat.completions.create(**args) 114 | 115 | 116 | def call_together(**kwargs): 117 | client = OpenAI( 118 | api_key=os.environ.get("TOGETHER_API_KEY"), 119 | base_url="https://api.together.xyz/v1", 120 | ) 121 | 122 | return client.chat.completions.create(**kwargs) 123 | 124 | 125 | def format_event(event: str, data: str): 126 | return f"""data: {data} 127 | event: {event} 128 | 129 | """ 130 | 131 | 132 | async def handle_anthropic_stream(stream, history): 133 | for chunk in stream.text_stream: 134 | yield format_event("chunk", chunk) 135 | response = stream.get_final_message() 136 | history.append( 137 | { 138 | "role": response.role, 139 | "content": [ 140 | c.model_dump() if hasattr(c, "model_dump") else c 141 | for c in response.content 142 | ], 143 | } 144 | ) 145 | yield response 146 | 147 | 148 | def process_anthropic_tool_results(tool_results, history): 149 | for result in tool_results: 150 | content = [] 151 | for c in result.get("content", []): 152 | content.append(c.model_dump() if hasattr(c, "model_dump") else c) 153 | result["content"] = content 154 | history.append(result) 155 | return tool_results 156 | 157 | 158 | async def handle_default_stream(stream, history): 159 | response = ToolhouseStreamStorage() 160 | for chunk in stream: 161 | response.add(chunk) 162 | if chunk.choices[0].delta.content is not None: 163 | yield format_event("chunk", chunk.choices[0].delta.content) 164 | 165 | completion = stream_to_chat_completion(response) 166 | if completion: 167 | chat = { 168 | "role": completion.choices[0].message.role, 169 | } 170 | 171 | if completion.choices[0].message.content: 172 | chat["content"] = completion.choices[0].message.content 173 | 174 | if completion.choices[0].message.tool_calls: 175 | chat["tool_calls"] = [ 176 | t.model_dump() for t in completion.choices[0].message.tool_calls 177 | ] 178 | 179 | history.append(chat) 180 | 181 | yield response 182 | 183 | 184 | def process_default_tool_results(tool_results, history): 185 | history.extend(tool_results) 186 | return tool_results 187 | 188 | 189 | def sanitize_history(history, provider): 190 | if provider == "anthropic": 191 | return json.dumps(history) 192 | 193 | result = [] 194 | for i in range(len(history)): 195 | if i == len(history) - 1 or history[i]["role"] != history[i + 1]["role"]: 196 | result.append(history[i]) 197 | return json.dumps(result) 198 | 199 | 200 | async def generate_stream( 201 | messages: list, model: str, bundle: str = "default", email: str | None = None 202 | ): 203 | try: 204 | current_messages = messages.copy() 205 | history = messages.copy() 206 | provider = get_model(model).get("provider") 207 | th = Toolhouse(provider="anthropic") 208 | 209 | tools = th.get_tools(bundle) 210 | tool_results = [] 211 | 212 | while True: 213 | with llm_call( 214 | model=model, 215 | system=system_prompt(tools), 216 | max_tokens=8192, 217 | messages=current_messages, 218 | stream=True, 219 | stop="", 220 | ) as stream: 221 | response = None 222 | async for chunk in ( 223 | handle_anthropic_stream(stream, history) 224 | if provider == "anthropic" 225 | else handle_default_stream(stream, history) 226 | ): 227 | if isinstance(chunk, str): 228 | yield chunk 229 | else: 230 | response = chunk 231 | 232 | response = stream_to_chat_completion(response) 233 | tool_results.append( 234 | { 235 | "role": "assistant", 236 | "content": response.choices[0].message.content, 237 | } 238 | ) 239 | 240 | if tool_call := find_tool_use(response.choices[0].message.content): 241 | print("Using tool:", tool_call.get("tool_name")) 242 | tool_response = run_tool(tool_call) 243 | tool_results.append( 244 | { 245 | "role": "user", 246 | "content": f"Response from {tool_call.get("tool_name")}: {tool_response.get("content").get("content")}", 247 | } 248 | ) 249 | 250 | if provider == "anthropic": 251 | tool_results = process_anthropic_tool_results(tool_results, history) 252 | else: 253 | tool_results = process_default_tool_results(tool_results, history) 254 | 255 | # Update messages for next iteration 256 | current_messages.extend(tool_results) 257 | 258 | if ( 259 | len(history) > 0 260 | and history[-1].get("role") == "assistant" 261 | and response.choices[0].finish_reason == "stop" 262 | ): 263 | yield format_event("end", sanitize_history(history, provider)) 264 | return 265 | except Exception as e: 266 | traceback.print_exc() 267 | yield str(e) 268 | -------------------------------------------------------------------------------- /llms/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/llms/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from starlette.applications import Starlette 2 | from starlette.requests import Request 3 | from starlette.responses import FileResponse, HTMLResponse 4 | from starlette.routing import Route 5 | from starlette.routing import Mount 6 | from starlette.staticfiles import StaticFiles 7 | from starlette.middleware import Middleware 8 | from starlette.middleware.base import BaseHTTPMiddleware 9 | from helpers import read_config 10 | import api.chat 11 | import dotenv 12 | import os 13 | import pathlib 14 | 15 | dotenv.load_dotenv() 16 | 17 | 18 | class DisableCacheMiddleware(BaseHTTPMiddleware): 19 | async def dispatch(self, request, call_next): 20 | response = await call_next(request) 21 | response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 22 | response.headers["Pragma"] = "no-cache" 23 | response.headers["Expires"] = "0" 24 | return response 25 | 26 | 27 | async def serve_index(request): 28 | return FileResponse("static/index.html") 29 | 30 | 31 | def get_app_name(request: Request): 32 | path_segments = request.url.path.strip("/").split("/") 33 | return path_segments[-1] if path_segments else None 34 | 35 | 36 | async def serve_static(request: Request): 37 | # Extract the requested path 38 | path = request.path_params.get("path", "") 39 | request 40 | file_path = pathlib.Path("static") / path 41 | 42 | # Check if the file exists 43 | if file_path.is_file(): 44 | return FileResponse(file_path) 45 | 46 | appname = get_app_name(request) 47 | if read_config(f"./prompts/{appname}.toml"): 48 | with open("static/index.html", "r") as file: 49 | html_content = file.read() 50 | page = html_content.replace( 51 | '', 52 | f'', 53 | ) 54 | page = page.replace( 55 | '', 56 | f'', 57 | ) 58 | return HTMLResponse(page) 59 | 60 | # Default to serving the index.html 61 | return FileResponse("static/index.html", status_code=404) 62 | 63 | 64 | # Determine middleware and debug based on environment 65 | middleware = ( 66 | [Middleware(DisableCacheMiddleware)] 67 | if os.environ.get("ENVIRONMENT") == "development" 68 | else [] 69 | ) 70 | 71 | debug = os.environ.get("ENVIRONMENT") == "development" 72 | 73 | app = Starlette( 74 | debug=debug, 75 | middleware=middleware, 76 | routes=[ 77 | Route("/api/chat", api.chat.post, methods=["POST"]), 78 | Route("/app/{path:path}", serve_static), 79 | ], 80 | ) 81 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "simplechat" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Daniele"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | starlette = "^0.41.3" 11 | uvicorn = "^0.32.1" 12 | black = "^24.10.0" 13 | toolhouse = "^1.3.0" 14 | openai = "^1.57.0" 15 | anthropic = "^0.40.0" 16 | python-dotenv = "^1.0.1" 17 | h2 = "^4.1.0" 18 | uvloop = "^0.21.0" 19 | httptools = "^0.6.4" 20 | hypercorn = "^0.17.3" 21 | httpx = "^0.28.1" 22 | certifi = "^2024.8.30" 23 | 24 | 25 | [build-system] 26 | requires = ["poetry-core"] 27 | build-backend = "poetry.core.masonry.api" 28 | -------------------------------------------------------------------------------- /static/components/action-box.js: -------------------------------------------------------------------------------- 1 | import Domo, { html } from "/app/domo.js"; 2 | 3 | export class ActionBox extends Domo { 4 | handleClick(e) { 5 | e.stopImmediatePropagation(); 6 | const key = e.currentTarget.dataset.key; 7 | const prompt = config.suggested_actions[key].action; 8 | this.actionHandler(prompt); 9 | } 10 | 11 | render() { 12 | if (this.dataset.hidden === "true") { 13 | return null; 14 | } 15 | return config.suggested_actions.map( 16 | ({ title, label }, key) => html` ` 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /static/components/chat-history-container.js: -------------------------------------------------------------------------------- 1 | import Domo, {html} from '/app/domo.js'; 2 | 3 | export class ChatHistoryContainer extends Domo { 4 | constructor(component) { 5 | super(component); 6 | this.md = new showdown.Converter({ 7 | smoothLivePreview: true, 8 | openLinksInNewWindow: true, 9 | emoji: true, 10 | underline: true, 11 | strikethrough: true, 12 | backslashEscapesHTMLTags: true, 13 | ghMentions: false, 14 | ghMentionsLink: '/#', 15 | extensions: [{ 16 | type: 'lang', 17 | filter: (text, converter, options) => { 18 | const tagsToReplace = { 19 | '&': '&', 20 | '<': '<', 21 | '>': '>' 22 | }; 23 | 24 | const replaceTag = (tag) => tagsToReplace[tag] || tag; 25 | return text.replace(/[&<>]/g, replaceTag); 26 | } 27 | }] 28 | }); 29 | 30 | this.md.setFlavor('github'); 31 | this.animations = new Set() 32 | } 33 | 34 | makeHtml(chat) { 35 | switch (this.dataset.provider) { 36 | case 'anthropic': 37 | return this.makeHtmlAnthropic(chat.role, chat.content); 38 | case 'openai': 39 | return this.makeHtmlOpenAI(chat); 40 | } 41 | } 42 | 43 | cssClassNames(role, text = '') { 44 | const classes = [role]; 45 | if (text.includes('')) { 46 | classes.push('success'); 47 | } else if (text.includes('')) { 48 | classes.push('error'); 49 | } 50 | 51 | return classes.join(' ') 52 | } 53 | 54 | makeHtmlOpenAI({ role, content, tool_calls}) { 55 | if (content) { 56 | if (role === 'assistant') { 57 | if (content.includes('') && content.includes('')) { 58 | content = content.replace(/.*?<\/think>/gs, '').trim(); 59 | } else if (content.includes('')) { 60 | content = content.trim().replace(/.*$/gs, '').trim(); 61 | } 62 | } 63 | return html`
${this.md.makeHtml(content)}
`; 64 | } else if (Array.isArray(tool_calls)) { 65 | const bubbles = tool_calls.map((c) => 66 | `
67 |

Using tools

68 |

${c.function.name}(${c.function.arguments != '{}' ? c.function.arguments : ''})

69 |
` 70 | ).join(''); 71 | 72 | return html`${bubbles}`; 73 | } 74 | } 75 | 76 | makeHtmlAnthropic(role, content) { 77 | let bubbles = ''; 78 | if (Array.isArray(content)) { 79 | content.forEach((c) => { 80 | switch(c.type) { 81 | case 'text': 82 | if (c.text) { 83 | bubbles += `
${this.md.makeHtml(c.text)}
`; 84 | } 85 | break; 86 | case 'tool_use': 87 | bubbles += ` 88 |
89 |

Using tools

90 |

${c.name}(${JSON.stringify(c.input) !== '{}' ? JSON.stringify(c.input) : ''})

91 |
`; 92 | break; 93 | } 94 | }); 95 | return html`${bubbles}`; 96 | } else { 97 | const text = content.text ?? content; 98 | return html`
${this.md.makeHtml(text)}
`; 99 | } 100 | } 101 | 102 | componentDidRender() { 103 | this.scrollTop = this.scrollHeight; 104 | } 105 | 106 | render() { 107 | const msgs = this.getHistory() 108 | .filter(({role}) => ['user', 'assistant'].includes(role)) 109 | .map((chat) => { 110 | return this.makeHtml(chat); 111 | }); 112 | if (this.thinking()) { 113 | msgs.push(html`

Thinking…

`) 114 | } else if (this.getStream()) { 115 | msgs.push(html`
${this.md.makeHtml(this.getStream())}
`) 116 | } 117 | 118 | return msgs; 119 | } 120 | } -------------------------------------------------------------------------------- /static/components/components.js: -------------------------------------------------------------------------------- 1 | export { ActionBox } from "./action-box.js"; 2 | export { ChatHistoryContainer } from "./chat-history-container.js"; 3 | export { MainApp } from "./main-app.js"; 4 | export { ResizableTextarea } from "./resizable-textarea.js"; 5 | export { SuccessMessage } from "./success-message.js"; 6 | export { ToolhouseIcon } from "./toolhouse-icon.js"; -------------------------------------------------------------------------------- /static/components/main-app.js: -------------------------------------------------------------------------------- 1 | import Domo, { html } from "/app/domo.js"; 2 | import { StreamProcessor } from "/app/helpers/stream.js"; 3 | 4 | export class MainApp extends Domo { 5 | constructor(component) { 6 | super(component); 7 | this.firstRender = true; 8 | } 9 | 10 | getInitialState() { 11 | this.setupStream(); 12 | const state = { 13 | messages: [], 14 | thinking: false, 15 | streamingResponse: "", 16 | formIsHidden: false, 17 | email: "", 18 | configured: false, 19 | }; 20 | 21 | return state; 22 | } 23 | 24 | setupStream() { 25 | this.processor = new StreamProcessor("/api/chat"); 26 | this.processor.addEventListener("chunk", (event) => { 27 | this.setState({ 28 | thinking: false, 29 | streamingResponse: this.state.streamingResponse + event.detail, 30 | }); 31 | }); 32 | 33 | this.processor.addEventListener("end", (event) => { 34 | const messages = JSON.parse(event.detail); 35 | const lastMessage = JSON.stringify(messages.at(-1)); 36 | this.setState({ streamingResponse: "", messages: messages }); 37 | }); 38 | } 39 | 40 | handleMessageSubmission(value) { 41 | if (value.trim().length === 0) { 42 | return; 43 | } 44 | const messages = this.state.messages; 45 | messages.push({ role: "user", content: value.trim() }); 46 | this.setState({ messages: messages, thinking: true }); 47 | 48 | const postData = { 49 | messages: this.state.messages, 50 | }; 51 | 52 | console.log(postData); 53 | this.processor.processStream(postData); 54 | } 55 | 56 | getMessages() { 57 | return this.state.messages; 58 | } 59 | getStreamingResponse() { 60 | if (this.state.streamingResponse.includes("") && this.state.streamingResponse.includes("")) { 61 | return this.state.streamingResponse.replace(/.*?<\/think>/gs, "").trim(); 62 | } if (this.state.streamingResponse.includes("")) { 63 | return ""; 64 | } 65 | } 66 | thinking() { 67 | return this.state.thinking; 68 | } 69 | 70 | submitPreferences({ email, preferences }) { 71 | } 72 | 73 | componentDidRender() { 74 | this.firstRender = false; 75 | } 76 | 77 | render() { 78 | return html` 79 |
80 | 81 |

DeepSeek + Toolhouse

82 |
83 | 84 | ${this.state.configured && this.firstRender ? `` : ""} 85 | 92 | 96 | 97 | `; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /static/components/resizable-textarea.js: -------------------------------------------------------------------------------- 1 | import Domo, {html} from '/app/domo.js'; 2 | 3 | export class ResizableTextarea extends Domo { 4 | constructor(component) { 5 | super(component); 6 | } 7 | 8 | autoResizeTextbox(e) { 9 | if (this.textbox.scrollHeight > this.textbox.offsetHeight) { 10 | this.textbox.style.height = this.textbox.scrollHeight + 'px'; 11 | } 12 | } 13 | 14 | maybeSubmit(e) { 15 | if (e.key === 'Enter' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { 16 | e.preventDefault(); 17 | super.setState({chatValue: e.target.value}); 18 | this.enterHandler(e.target.value); 19 | e.target.value = ''; 20 | this.textbox.style.height = this.initialHeight; 21 | } else if (e.key === 'Enter' && (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey)) { 22 | e.preventDefault(); 23 | e.target.value += '\n'; 24 | e.target.selectionStart = e.target.selectionEnd = e.target.value.length; 25 | this.autoResizeTextbox(e); 26 | } 27 | } 28 | 29 | componentDidRender() { 30 | this.textbox = this.querySelector('#auto-resize-textbox'); 31 | this.initialHeight = this.textbox.initialHeight + 'px'; 32 | setTimeout(() => { 33 | this.textbox.focus(); 34 | }, 100); 35 | } 36 | 37 | render() { 38 | return html` 39 | 40 | `; 41 | } 42 | } -------------------------------------------------------------------------------- /static/components/success-message.js: -------------------------------------------------------------------------------- 1 | import Domo, {html} from '/app/domo.js'; 2 | 3 | export class SuccessMessage extends Domo { 4 | render() { 5 | return this.dataset.hidden ? 6 | null : 7 | html`

You're all set

8 |

${config.main.all_set_label}

`; 9 | } 10 | } -------------------------------------------------------------------------------- /static/components/toolhouse-icon.js: -------------------------------------------------------------------------------- 1 | import Domo, { html } from "/app/domo.js"; 2 | 3 | export class ToolhouseIcon extends Domo { 4 | render() { 5 | return html` 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | `; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/css/components/resizable-textarea.css: -------------------------------------------------------------------------------- 1 | #auto-resize-textbox { 2 | width: 100%; 3 | padding: 10px; 4 | border: 1px solid #ccc; 5 | border-radius: 5px; 6 | background-color: transparent; 7 | font-family: cinetype; 8 | color: inherit; 9 | overflow-y: hidden; 10 | box-sizing: border-box; 11 | outline: none; 12 | } -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: cinetype; 3 | src: url('/app/fonts/GT-Cinetype-Mono.woff') format('woff'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @keyframes fadeAndSlide { 9 | from { 10 | opacity: 0; 11 | transform: translateY(20px); 12 | } 13 | to { 14 | opacity: 1; 15 | transform: translateY(0); 16 | } 17 | } 18 | 19 | @keyframes fadeIn { 20 | from { 21 | opacity: 0; 22 | } 23 | to { 24 | opacity: 1; 25 | } 26 | } 27 | 28 | @keyframes fadeOut { 29 | from { 30 | opacity: 1; 31 | } 32 | to { 33 | opacity: 0; 34 | } 35 | } 36 | 37 | @keyframes blink { 38 | 0% { 39 | opacity: 1; /* Starting with a faint opacity to simulate old monitor effect */ 40 | } 41 | 50% { 42 | opacity: 1; /* Fully opaque at the midpoint */ 43 | } 44 | 51% { 45 | opacity: 0.5; /* Fully opaque at the midpoint */ 46 | } 47 | 100% { 48 | opacity: 0.1; /* Ending with a faint opacity */ 49 | } 50 | } 51 | 52 | .blink { 53 | animation: blink 1.5s infinite; 54 | } 55 | 56 | .fade-slide { 57 | animation: fadeAndSlide 0.3s ease-out forwards; 58 | } 59 | 60 | .fade-out { 61 | animation: fadeOut 0.3s ease-out forwards; 62 | } 63 | 64 | .fade-in { 65 | animation: fadeIn 0.3s ease-out forwards; 66 | } 67 | 68 | .fade-in.slow { 69 | animation-duration: 2s; 70 | } 71 | 72 | .animate-delay { 73 | animation-delay: 0.3s; 74 | } 75 | 76 | html * { 77 | font-family: cinetype, monospace; 78 | overscroll-behavior: none; 79 | } 80 | 81 | body { 82 | background-color: #000; 83 | color: #aaa; 84 | padding: 1rem; 85 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 86 | width: 60%; 87 | margin: 0 auto; 88 | } 89 | 90 | h1, h2, h3, h4, h5, h6 { 91 | color: #08f; 92 | text-shadow: 0 0 2px #000; 93 | } 94 | 95 | p, span, a { 96 | color: #aaa; 97 | } 98 | 99 | a { 100 | text-decoration: none; 101 | border-bottom: 1px dotted #5f8b6b; 102 | } 103 | 104 | a:hover { 105 | color: #fff; 106 | } 107 | 108 | form { 109 | display: flex; 110 | flex-direction: column; 111 | } 112 | 113 | form input, form textarea, form label, form button { 114 | display: block; 115 | margin: 0.5rem; 116 | color: #ccc 117 | } 118 | 119 | form textarea { 120 | height: 60px; 121 | resize: none; 122 | } 123 | 124 | button, input[type="button"], input[type="submit"] { 125 | background-color: #1f96ff; 126 | color: #fff; 127 | border: none; 128 | padding: 0.5rem 1rem; 129 | border-radius: 5px; 130 | cursor: pointer; 131 | } 132 | 133 | button:disabled { 134 | opacity: 0.5; 135 | } 136 | 137 | button:hover, input[type="button"]:hover, input[type="submit"]:hover { 138 | background-color: #08f; 139 | } 140 | 141 | input[type="text"], 142 | input[type="email"], 143 | input[type="password"], 144 | textarea { 145 | background-color: #222; 146 | color: #aaa; 147 | border: none; 148 | padding: 0.5rem; 149 | border-radius: 5px; 150 | } 151 | 152 | input:focus, textarea:focus { 153 | outline: 2px solid #08f; 154 | } 155 | 156 | /* Table Styles */ 157 | table { 158 | background-color: #222; 159 | border-collapse: collapse; 160 | width: 100%; 161 | } 162 | 163 | th, td { 164 | color: #5f8b6b; 165 | padding: 0.5rem; 166 | border: 1px solid #444; 167 | } 168 | 169 | th { 170 | background-color: #333; 171 | } 172 | 173 | li { 174 | padding: 0.5rem; 175 | } 176 | 177 | li:last-child { 178 | border-bottom: none; 179 | } 180 | 181 | /* Code Block Styles */ 182 | code { 183 | background-color: #22222288; 184 | color: #08f; 185 | padding: 0.5rem; 186 | border-radius: 5px; 187 | } 188 | 189 | pre { 190 | background-color: #222888; 191 | padding: 0.5rem; 192 | border-radius: 5px; 193 | overflow-x: auto; 194 | } 195 | 196 | pre code { 197 | background-color: transparent; 198 | } 199 | 200 | #auto-resize-textbox { 201 | width: 100%; 202 | padding: 10px; 203 | border: 1px solid #ccc; 204 | border-radius: 5px; 205 | background-color: transparent; 206 | font-family: cinetype; 207 | color: inherit; 208 | overflow-y: hidden; 209 | box-sizing: border-box; 210 | outline: none; 211 | } 212 | 213 | main-app { 214 | height: calc(100vh - 54px - 38px); 215 | display: flex; 216 | flex-direction: column; 217 | overflow: hidden; /* Add this */ 218 | } 219 | 220 | chat-history-container { 221 | flex: 1 1 auto; /* Change from flex: 2 */ 222 | display: flex; 223 | flex-direction: column; 224 | overflow-y: auto; 225 | min-height: 0; 226 | max-height: 100%; /* Add this */ 227 | } 228 | 229 | chat-history-container img { 230 | max-width: 100%; 231 | } 232 | 233 | chat-history-container > div { 234 | border-radius: 8px; 235 | margin: 0.5rem 0 1rem; 236 | padding: 0 1rem; 237 | color: #ccc; 238 | max-width: 60%; 239 | } 240 | 241 | chat-history-container div.user { 242 | margin-left: auto; 243 | margin-right: 0; 244 | background: #08f; 245 | } 246 | 247 | chat-history-container div.assistant { 248 | margin-left: 0; 249 | margin-right: auto; 250 | background: #181818; 251 | } 252 | 253 | .success, 254 | chat-history-container div.assistant.success { 255 | background: #3E8E41; 256 | } 257 | 258 | .error, 259 | chat-history-container div.assistant.error { 260 | background: hsl(0, 64%, 42%); 261 | } 262 | 263 | chat-history-container div.assistant h1, 264 | chat-history-container div.assistant h2, 265 | chat-history-container div.assistant h3, 266 | chat-history-container div.assistant h4, 267 | chat-history-container div.assistant h5, 268 | chat-history-container div.assistant h6, 269 | chat-history-container div.assistant p { 270 | color: #c8c8c8; 271 | } 272 | 273 | chat-history-container div.user h1, 274 | chat-history-container div.user h2, 275 | chat-history-container div.user h3, 276 | chat-history-container div.user h4, 277 | chat-history-container div.user h5, 278 | chat-history-container div.user h6, 279 | chat-history-container div.user p { 280 | color: #eee; 281 | } 282 | 283 | resizable-texarea { 284 | flex: 1; 285 | max-height: 200px; 286 | } 287 | 288 | success-message { 289 | background: #08f; 290 | padding: 2rem; 291 | text-align: center; 292 | border-radius: 5px; 293 | } 294 | 295 | success-message h1 { 296 | color: #fff; 297 | } 298 | 299 | success-message p { 300 | color: #c8c8c8; 301 | } 302 | 303 | action-box { 304 | /* display: flex; 305 | flex-wrap: wrap; */ 306 | gap: 1rem; 307 | display: grid; 308 | grid-template-columns: repeat(2, 1fr); 309 | margin: 1rem 0; 310 | } 311 | 312 | action-box button { 313 | /* flex-basis: calc(50% - 10px); 314 | max-width: calc(50% - 10px); */ 315 | text-align: left; 316 | color: #c8c8c8; 317 | padding: 1rem; 318 | } 319 | 320 | action-box button b { 321 | display: block; 322 | color: #fff; 323 | } 324 | 325 | toolhouse-icon svg { 326 | color: #08f; 327 | width: 2em; 328 | margin-right: 1rem 329 | } 330 | 331 | main#error { 332 | display: flex; 333 | flex-direction: column; 334 | justify-content: center; 335 | align-items: center; 336 | min-height: 100vh; 337 | } 338 | 339 | main#error.hidden { 340 | display: none; 341 | } 342 | 343 | main#error > * { 344 | text-align: center; 345 | opacity: 0; 346 | } 347 | 348 | main#error pre { 349 | background-color: #000; 350 | font-family: monospace; 351 | } 352 | 353 | @media only screen and (max-width: 768px) { 354 | html { 355 | font-size: 0.8rem; 356 | } 357 | 358 | body { 359 | width: auto; 360 | } 361 | 362 | chat-history-container > div { 363 | max-width: 80% 364 | } 365 | 366 | main-app { 367 | height: calc(100vh - 38px); 368 | } 369 | } -------------------------------------------------------------------------------- /static/domo.js: -------------------------------------------------------------------------------- 1 | const html = (strings, ...expressions) => { 2 | let result = ''; 3 | 4 | strings.forEach((string, i) => { 5 | result += string; 6 | 7 | if (i < expressions.length) { 8 | if (expressions[i] instanceof Function) { 9 | const fnName = expressions[i].name || `callback_${Math.random().toString(36).substr(2, 9)}`; 10 | result += fnName; 11 | } else { 12 | result += expressions[i]; 13 | } 14 | } 15 | }); 16 | 17 | result = result.replace( 18 | /<([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^>]*)?)\/>/g, 19 | '<$1$2>' 20 | ); 21 | 22 | const fragment = document.createRange().createContextualFragment(result); 23 | 24 | // Preserve component instance reference on elements 25 | Array.from(fragment.children).forEach(child => { 26 | if (child.tagName?.includes('-')) { 27 | child._parentComponentInstance = this; 28 | } 29 | }); 30 | 31 | return fragment; 32 | } 33 | 34 | const setupListeners = (component, element) => { 35 | // Set up listeners for the current element 36 | element.getAttributeNames() 37 | .filter(key => key.match(/^on\-/)) 38 | .forEach(key => { 39 | if (component[element.getAttribute(key)] instanceof Function) { 40 | element.addEventListener( 41 | key.replace('on-', ''), 42 | component[element.getAttribute(key)].bind(component), 43 | false 44 | ); 45 | } 46 | }); 47 | 48 | // Handle callbacks 49 | const cbAttributes = element.getAttributeNames() 50 | .filter(key => key.match(/^(cb\-)/)); 51 | 52 | cbAttributes.forEach(attr => { 53 | const callbackValue = element.getAttribute(attr); 54 | const methodName = attributeToCamelCase(attr.replace(/^(cb\-)/, '')); 55 | const componentMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(component)); 56 | const matchingMethod = componentMethods.find(method => 57 | method === callbackValue || 58 | component[method].name === callbackValue 59 | ); 60 | 61 | if (matchingMethod) { 62 | element[methodName] = function(...args) { 63 | return component[matchingMethod].apply(component, args); 64 | }; 65 | } 66 | }); 67 | 68 | // Recursively set up listeners for all children 69 | Array.from(element.children).forEach(child => { 70 | if (child.tagName && !child.tagName.includes('-')) { 71 | setupListeners(component, child); 72 | } 73 | }); 74 | } 75 | 76 | const diff = (currentDom, newDom, changes = {added: [], removed: []}) => { 77 | const currentLength = currentDom.children.length; 78 | const newLength = newDom.children.length; 79 | 80 | if (newLength === 0) { 81 | changes.removed = changes.removed.concat(Array.from(currentDom.children)); 82 | currentDom.replaceChildren(); 83 | return [currentDom, changes]; 84 | } 85 | 86 | if (currentLength === 0 && newLength > 0) { 87 | const newChildren = Array.from(newDom.children); 88 | changes.removed = changes.removed.concat(Array.from(currentDom.children)); 89 | changes.added = changes.added.concat(newChildren); 90 | currentDom.replaceChildren(...newChildren); 91 | return [currentDom, changes]; 92 | } 93 | 94 | if (currentLength > newLength) { 95 | for (let i = currentLength - 1; i >= newLength; i--) { 96 | const node = currentDom.children[i]; 97 | changes.removed.push(node.cloneNode(true)); 98 | node.parentNode.removeChild(node); 99 | } 100 | } else if (currentLength < newLength) { 101 | for (let i = currentLength; i < newLength; i++) { 102 | const node = newDom.children[i].cloneNode(true); 103 | changes.added.push(node); 104 | currentDom.appendChild(node); 105 | } 106 | } 107 | 108 | for (let i = 0; i < newLength; i++) { 109 | const currentNode = currentDom.children[i]; 110 | const newNode = newDom.children[i]; 111 | 112 | if (currentNode.children.length && newNode.children.length > 0) { 113 | diff(currentNode, newNode, changes); 114 | } 115 | 116 | if (currentNode.outerHTML !== newNode.outerHTML) { 117 | const newNodeClone = newNode.cloneNode(true); 118 | changes.removed.push(currentNode.cloneNode(true)); 119 | changes.added.push(newNodeClone); 120 | currentNode.replaceWith(newNodeClone); 121 | } 122 | } 123 | 124 | return [currentDom, changes]; 125 | } 126 | 127 | const classNameFromTag = tag => 128 | tag 129 | .split('-') 130 | .map(part => 131 | part 132 | .charAt(0) 133 | .toUpperCase() + 134 | part 135 | .slice(1) 136 | .toLowerCase()) 137 | .join(''); 138 | 139 | const attributeToCamelCase = (attr) => 140 | attr.split('-').map((part, index) => 141 | index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1) 142 | ).join(''); 143 | 144 | const init = async (el) => { 145 | if (!el.tagName?.includes('-')) { 146 | if (el.children) { 147 | for (const child of el.children) { 148 | await init(child); 149 | } 150 | } 151 | return; 152 | } 153 | 154 | const tag = el.tagName.toLowerCase(); 155 | const href = document.querySelector('link[rel="components"]')?.href; 156 | const path = el.getAttribute('module'); 157 | const module = await import(href || path); 158 | 159 | if (!customElements.get(tag)) { 160 | try { 161 | customElements.define(tag, href ? module[classNameFromTag(tag)] : module.default); 162 | await customElements.whenDefined(tag); 163 | } catch (e) { console.error(`Could not initialize <${tag}>. Check that the component exist and that is has been imported. (${e.message})`); } 164 | } 165 | }; 166 | 167 | const render = element => { 168 | if (element.componentWillRender.call(element)) { 169 | const template = element.render.call(element); 170 | 171 | // Create a temporary container for the new content 172 | const tempContainer = document.createElement('div'); 173 | 174 | // Handle array of templates 175 | if (Array.isArray(template)) { 176 | const combinedFragment = document.createDocumentFragment(); 177 | template.forEach(fragment => { 178 | if (fragment instanceof DocumentFragment) { 179 | combinedFragment.append(...Array.from(fragment.children)); 180 | } 181 | }); 182 | tempContainer.append(...Array.from(combinedFragment.children)); 183 | } 184 | // Handle single template 185 | else if (template instanceof DocumentFragment) { 186 | tempContainer.append(...Array.from(template.children)); 187 | } 188 | 189 | // Perform the diff and get the updated DOM 190 | const [updatedDom, changes] = diff(element, tempContainer); 191 | 192 | // Set up listeners for all new elements 193 | changes.added.forEach(node => { 194 | setupListeners(element, node); 195 | }); 196 | 197 | if (template) { 198 | element.componentDidRender.call(element); 199 | } 200 | } 201 | } 202 | 203 | export default class Domo extends HTMLElement { 204 | constructor() { 205 | super(); 206 | this.state = this.getInitialState(); 207 | this._renderPending = true; 208 | 209 | new MutationObserver(mutations => { 210 | mutations.forEach(mutation => { 211 | if (mutation.addedNodes) { 212 | Array 213 | .from(mutation.addedNodes) 214 | .map(el => { 215 | return !!init(el) && !!el.getAttributeNames && setupListeners(this, el) 216 | }) 217 | } 218 | 219 | if (mutation.type === 'attributes' && mutation.target.tagName.includes('-') && mutation.attributeName.match(/data-/)) { 220 | const datasetKey = mutation.attributeName.replace('data-', ''); 221 | mutation.newValue = mutation.target.getAttribute(mutation.attributeName); 222 | mutation.datasetKey = classNameFromTag(datasetKey); 223 | mutation.datasetKey = mutation.datasetKey.charAt(0).toLowerCase() + mutation.datasetKey.slice(1); 224 | mutation.target.didUpdateDataset(mutation); 225 | } 226 | }); 227 | }).observe(this, {attributes: true, childList: true, subtree: true, attributeOldValue: true}); 228 | 229 | init(this); 230 | } 231 | 232 | // Add this new lifecycle method 233 | attributeChangedCallback(name, oldValue, newValue) { 234 | if (this._renderPending) { 235 | this._setupCallbacks(); 236 | } 237 | } 238 | 239 | connectedCallback() { 240 | this._setupCallbacks(); 241 | if (this._renderPending) { 242 | this._renderPending = false; 243 | render(this); 244 | } 245 | } 246 | 247 | _setupCallbacks() { 248 | // Find the parent component instance 249 | let parent = this.parentElement; 250 | while (parent && !parent.tagName?.includes('-')) { 251 | parent = parent.parentElement; 252 | } 253 | 254 | if (parent) { 255 | // Get all cb- attributes 256 | const cbAttributes = this.getAttributeNames() 257 | .filter(key => key.match(/^(cb\-)/)); 258 | 259 | cbAttributes.forEach(attr => { 260 | const callbackValue = this.getAttribute(attr); 261 | const methodName = attributeToCamelCase(attr.replace(/^(cb\-)/, '')); 262 | 263 | // Look for the method on the parent 264 | if (parent[callbackValue] instanceof Function) { 265 | this[methodName] = (...args) => parent[callbackValue].apply(parent, args); 266 | } else { 267 | // Look through parent's prototype methods 268 | const parentMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(parent)); 269 | const matchingMethod = parentMethods.find(method => 270 | method === callbackValue || 271 | parent[method]?.name === callbackValue 272 | ); 273 | 274 | if (matchingMethod) { 275 | this[methodName] = (...args) => parent[matchingMethod].apply(parent, args); 276 | } 277 | } 278 | }); 279 | } 280 | } 281 | 282 | setState(value) { 283 | const newstate = JSON.stringify(value); 284 | if (newstate === null) { 285 | return true; 286 | } 287 | 288 | const oldstate = JSON.stringify(this.state); 289 | if (oldstate !== newstate) { 290 | this.state = Object.assign(this.state, JSON.parse(newstate)); 291 | this.stateDidChange(); 292 | this._renderPending = true; 293 | render(this); 294 | } 295 | } 296 | 297 | stateDidChange() { } 298 | getInitialState() { return { } } 299 | componentWillRender() { return true } 300 | didUpdateDataset(mutation) { } 301 | componentDidRender() { } 302 | render() { } 303 | } 304 | 305 | export { init, html, diff }; -------------------------------------------------------------------------------- /static/fonts/GT-Cinetype-Mono.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/static/fonts/GT-Cinetype-Mono.woff -------------------------------------------------------------------------------- /static/fonts/WebPlus_IBM_VGA_8x16-2x.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/static/fonts/WebPlus_IBM_VGA_8x16-2x.woff -------------------------------------------------------------------------------- /static/fonts/WebPlus_IBM_VGA_8x16.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toolhouse-community/deepseek-tool-use-experiment/bdeab57d159e966252ddb2d6b09700b4cff20790/static/fonts/WebPlus_IBM_VGA_8x16.woff -------------------------------------------------------------------------------- /static/helpers/stream.js: -------------------------------------------------------------------------------- 1 | export class StreamProcessor extends EventTarget { 2 | constructor(url) { 3 | super(); 4 | this.url = url; 5 | } 6 | 7 | async processStream(postData) { 8 | try { 9 | const response = await fetch(this.url, { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | }, 14 | body: JSON.stringify(postData) 15 | }); 16 | 17 | if (!response.body) { 18 | throw new Error('ReadableStream not supported'); 19 | } 20 | 21 | const reader = response.body.getReader(); 22 | const decoder = new TextDecoder(); 23 | let buffer = ''; 24 | 25 | const stream = new ReadableStream({ 26 | start: async (controller) => { 27 | try { 28 | let isStreamActive = true; 29 | while (isStreamActive) { 30 | try { 31 | const { done, value } = await reader.read(); 32 | 33 | if (done) { 34 | isStreamActive = false; 35 | break; 36 | } 37 | 38 | buffer += decoder.decode(value, { stream: true }); 39 | 40 | // Look for complete event pairs (data + event) 41 | while (true) { 42 | const eventIndex = buffer.indexOf('event:'); 43 | if (eventIndex === -1) break; 44 | 45 | // Find the next double newline after 'event:' 46 | const endIndex = buffer.indexOf('\n\n', eventIndex); 47 | if (endIndex === -1) break; 48 | 49 | // Extract the complete event pair 50 | const eventPair = buffer.substring(0, endIndex + 2); 51 | buffer = buffer.substring(endIndex + 2); 52 | 53 | // Parse the event pair 54 | const dataMatch = eventPair.match(/data:([\s\S]*?)(?=\nevent:)/); 55 | const eventMatch = eventPair.match(/event: (\w+)/); 56 | 57 | if (dataMatch && eventMatch) { 58 | const eventData = dataMatch[1]; 59 | const eventType = eventMatch[1]; 60 | 61 | // Emit custom event 62 | const customEvent = new CustomEvent(eventType, { 63 | detail: eventData 64 | }); 65 | this.dispatchEvent(customEvent); 66 | 67 | if (eventType === 'chunk') { 68 | controller.enqueue(eventData); 69 | } else if (eventType === 'end') { 70 | isStreamActive = false; 71 | break; 72 | } 73 | } 74 | } 75 | } catch (readError) { 76 | if (readError.message.includes('reader has been released')) { 77 | isStreamActive = false; 78 | break; 79 | } 80 | throw readError; 81 | } 82 | } 83 | 84 | controller.close(); 85 | if (!reader.closed) { 86 | reader.releaseLock(); 87 | } 88 | } catch (error) { 89 | controller.error(error); 90 | if (!reader.closed) { 91 | reader.releaseLock(); 92 | } 93 | const errorEvent = new CustomEvent('error', { 94 | detail: error 95 | }); 96 | this.dispatchEvent(errorEvent); 97 | } 98 | }, 99 | 100 | cancel() { 101 | if (!reader.closed) { 102 | reader.releaseLock(); 103 | } 104 | } 105 | }); 106 | 107 | if (window.onbeforeunload) { 108 | window.onbeforeunload(() => stream.cancel()); 109 | } 110 | 111 | return stream; 112 | } catch (error) { 113 | const errorEvent = new CustomEvent('error', { 114 | detail: error 115 | }); 116 | this.dispatchEvent(errorEvent); 117 | throw error; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 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 |

Your browser hallucinated.

55 |
56 | Back to Toolhouse 57 |
58 |
59 | 87 | --------------------------------------------------------------------------------