├── .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 | 
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 |
39 |
40 |
41 |
42 |
43 |
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`
20 | ${title}
21 | ${label}
22 | `
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``)
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>$1>'
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 |
58 |
59 |
87 |
--------------------------------------------------------------------------------