mulebot 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mulebot-0.2.0 → mulebot-0.3.0}/PKG-INFO +1 -1
- mulebot-0.3.0/mulebot/__init__.py +5 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/agent.py +29 -24
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/cli.py +4 -2
- mulebot-0.3.0/mulebot/providers.py +171 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/server.py +12 -9
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/tools.py +5 -2
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/ui/static/app.js +7 -3
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/ui/templates/index.html +1 -1
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/PKG-INFO +1 -1
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/SOURCES.txt +1 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/setup.py +1 -1
- mulebot-0.2.0/mulebot/__init__.py +0 -4
- {mulebot-0.2.0 → mulebot-0.3.0}/LICENSE +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/MANIFEST.in +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/__main__.py +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/assets.py +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/datasets.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/docs.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/index.yaml +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/products.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/readme.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/schema.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/sec-filings-lookup.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/sec_filings_archive.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/sec_filings_notifications.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/transfer_to_your_cloud.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/docs/utils.md +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/tool.py +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot/ui/static/style.css +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/dependency_links.txt +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/entry_points.txt +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/requires.txt +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/mulebot.egg-info/top_level.txt +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/pyproject.toml +0 -0
- {mulebot-0.2.0 → mulebot-0.3.0}/setup.cfg +0 -0
|
@@ -7,9 +7,10 @@ from time import perf_counter, time
|
|
|
7
7
|
|
|
8
8
|
from .tools import Tools, documentation_context
|
|
9
9
|
from .tool import validate_tools
|
|
10
|
+
from .providers import DEFAULT_MODELS, build_request, credentials, normalize_response, resolve_model
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
DEFAULT_MODEL = "
|
|
13
|
+
DEFAULT_MODEL = DEFAULT_MODELS["openrouter"]
|
|
13
14
|
TOOL_LIMIT = 50
|
|
14
15
|
|
|
15
16
|
|
|
@@ -63,15 +64,13 @@ def reasoning_text(response):
|
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class Agent:
|
|
66
|
-
def __init__(self,
|
|
67
|
-
self.
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise ValueError("Set DATAMULE_API_KEY and OPENROUTER_API_KEY.")
|
|
72
|
-
self._tool_state = Tools(api_key, allowed_tables=allowed_tables)
|
|
67
|
+
def __init__(self, provider, *, model=None, allowed_tables=None):
|
|
68
|
+
self.provider = provider
|
|
69
|
+
self.model = resolve_model(provider, model)
|
|
70
|
+
self.model_api_key = credentials(provider)
|
|
71
|
+
self._tool_state = Tools(allowed_tables=allowed_tables)
|
|
73
72
|
self.instructions = INSTRUCTIONS
|
|
74
|
-
self.
|
|
73
|
+
self.documentation = documentation_context()
|
|
75
74
|
self.messages = []
|
|
76
75
|
self._update_system_message()
|
|
77
76
|
|
|
@@ -100,17 +99,17 @@ class Agent:
|
|
|
100
99
|
raise TypeError("Instructions must be a string.")
|
|
101
100
|
return self.set_instructions("\n\n".join(part for part in (self.instructions, text) if part))
|
|
102
101
|
|
|
103
|
-
def
|
|
102
|
+
def set_documentation(self, text):
|
|
104
103
|
if not isinstance(text, str):
|
|
105
104
|
raise TypeError("Documentation must be a string.")
|
|
106
|
-
self.
|
|
105
|
+
self.documentation = text
|
|
107
106
|
self._update_system_message()
|
|
108
107
|
return self
|
|
109
108
|
|
|
110
|
-
def
|
|
109
|
+
def append_documentation(self, text):
|
|
111
110
|
if not isinstance(text, str):
|
|
112
111
|
raise TypeError("Documentation must be a string.")
|
|
113
|
-
return self.
|
|
112
|
+
return self.set_documentation("\n\n".join(part for part in (self.documentation, text) if part))
|
|
114
113
|
|
|
115
114
|
def set_tools(self, tools):
|
|
116
115
|
self._tool_state.registry = validate_tools(tools)
|
|
@@ -123,7 +122,7 @@ class Agent:
|
|
|
123
122
|
|
|
124
123
|
def _update_system_message(self):
|
|
125
124
|
content = (f"Today is {date.today().isoformat()}.\n\n<documentation>\n"
|
|
126
|
-
+ self.
|
|
125
|
+
+ self.documentation + "\n</documentation>\n\n" + self.instructions)
|
|
127
126
|
self.messages[:1] = [{"role": "system", "content": content}]
|
|
128
127
|
|
|
129
128
|
def export_state(self):
|
|
@@ -168,6 +167,13 @@ class Agent:
|
|
|
168
167
|
entry["reasoning_details"] = message["reasoning_details"]
|
|
169
168
|
elif isinstance(message.get("reasoning"), str):
|
|
170
169
|
entry["reasoning"] = message["reasoning"]
|
|
170
|
+
if "provider_data" in message:
|
|
171
|
+
data = message["provider_data"]
|
|
172
|
+
if (not isinstance(data, dict) or data.get("provider") not in ("gemini", "anthropic")
|
|
173
|
+
or not isinstance(data.get("content"), list)
|
|
174
|
+
or not all(isinstance(block, dict) for block in data["content"])):
|
|
175
|
+
raise ValueError("Invalid saved provider content.")
|
|
176
|
+
entry["provider_data"] = data
|
|
171
177
|
restored.append(entry)
|
|
172
178
|
for result in results.values():
|
|
173
179
|
if (not isinstance(result, dict) or not isinstance(result.get("sql"), str)
|
|
@@ -199,17 +205,14 @@ class Agent:
|
|
|
199
205
|
"These results are already saved and remain available even if older chat was trimmed. "
|
|
200
206
|
"Use query(result_id=...) or plot.sql to read them locally instead of paying to fetch "
|
|
201
207
|
"the same data again. Local SQL reads the table data using SQLite.\n" + json.dumps(saved)})
|
|
202
|
-
payload =
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
if self._tool_state.registry:
|
|
208
|
-
payload.update(tools=self._tool_state.definitions, tool_choice=tool_choice, parallel_tool_calls=False)
|
|
208
|
+
url, headers, payload = build_request(
|
|
209
|
+
self.provider, self.model, self.model_api_key, messages, self._tool_state.definitions,
|
|
210
|
+
tool_choice, os.environ.get("MULEBOT_REASONING_EFFORT"),
|
|
211
|
+
)
|
|
209
212
|
request = urllib.request.Request(
|
|
210
|
-
|
|
213
|
+
url,
|
|
211
214
|
data=json.dumps(payload).encode("utf-8"),
|
|
212
|
-
headers=
|
|
215
|
+
headers=headers,
|
|
213
216
|
)
|
|
214
217
|
try:
|
|
215
218
|
with urllib.request.urlopen(request, timeout=180) as response:
|
|
@@ -217,7 +220,7 @@ class Agent:
|
|
|
217
220
|
except urllib.error.HTTPError as exc:
|
|
218
221
|
detail = exc.read().decode("utf-8", errors="replace")[:2000]
|
|
219
222
|
raise RuntimeError(self._redact(f"Model request failed ({exc.code}): {detail}")) from None
|
|
220
|
-
return result
|
|
223
|
+
return normalize_response(self.provider, result)
|
|
221
224
|
|
|
222
225
|
def run(self, message):
|
|
223
226
|
"""Yield JSON-compatible events, independent of the CLI or browser UI."""
|
|
@@ -272,6 +275,8 @@ class Agent:
|
|
|
272
275
|
assistant["reasoning_details"] = response["reasoning_details"]
|
|
273
276
|
elif response.get("reasoning"):
|
|
274
277
|
assistant["reasoning"] = response["reasoning"]
|
|
278
|
+
if response.get("provider_data"):
|
|
279
|
+
assistant["provider_data"] = response["provider_data"]
|
|
275
280
|
if not calls and not assistant["content"]:
|
|
276
281
|
raise RuntimeError("The model returned no answer. Try again or choose another model.")
|
|
277
282
|
self.messages.append(assistant)
|
|
@@ -3,13 +3,15 @@ import json
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
from .agent import Agent
|
|
6
|
+
from .providers import DEFAULT_MODELS
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def main():
|
|
9
10
|
if hasattr(sys.stdout, "reconfigure"):
|
|
10
11
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
11
12
|
parser = argparse.ArgumentParser(description="Chat with Datamule's APIs.")
|
|
12
|
-
parser.add_argument("--
|
|
13
|
+
parser.add_argument("--provider", required=True, choices=DEFAULT_MODELS, help="Model provider.")
|
|
14
|
+
parser.add_argument("--model", help="Model ID; defaults to MULEBOT_MODEL or the provider's default.")
|
|
13
15
|
parser.add_argument("--tables", help="Optional comma-separated table allowlist.")
|
|
14
16
|
commands = parser.add_subparsers(dest="command", required=True)
|
|
15
17
|
chat = commands.add_parser("chat", help="Print raw events as newline-delimited JSON.")
|
|
@@ -17,7 +19,7 @@ def main():
|
|
|
17
19
|
serve = commands.add_parser("serve", help="Run the local browser interface.")
|
|
18
20
|
serve.add_argument("--port", type=int, default=8000)
|
|
19
21
|
args = parser.parse_args()
|
|
20
|
-
settings = {"model": args.model, "allowed_tables": None}
|
|
22
|
+
settings = {"provider": args.provider, "model": args.model, "allowed_tables": None}
|
|
21
23
|
if args.tables is not None:
|
|
22
24
|
settings["allowed_tables"] = [name.strip() for name in args.tables.split(",") if name.strip()]
|
|
23
25
|
try:
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Translate the shared agent history to each provider's HTTP API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_MODELS = {
|
|
10
|
+
"openrouter": "deepseek/deepseek-v4.1-flash-1017:nitro",
|
|
11
|
+
"openai": "gpt-5.4-mini",
|
|
12
|
+
"gemini": "gemini-3.8-flash",
|
|
13
|
+
"anthropic": "claude-sonnet-5-5",
|
|
14
|
+
}
|
|
15
|
+
KEY_VARIABLES = {
|
|
16
|
+
"openrouter": "OPENROUTER_API_KEY", "openai": "OPENAI_API_KEY",
|
|
17
|
+
"gemini": "GEMINI_API_KEY", "anthropic": "ANTHROPIC_API_KEY",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_provider(provider):
|
|
22
|
+
if not isinstance(provider, str) or provider not in DEFAULT_MODELS:
|
|
23
|
+
raise ValueError("Choose a provider: " + ", ".join(DEFAULT_MODELS) + ".")
|
|
24
|
+
return provider
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_model(provider, model=None):
|
|
28
|
+
validate_provider(provider)
|
|
29
|
+
model = model if model is not None else os.environ.get("MULEBOT_MODEL") or DEFAULT_MODELS[provider]
|
|
30
|
+
if not isinstance(model, str) or not model.strip() or len(model) > 200 or any(c.isspace() for c in model.strip()):
|
|
31
|
+
raise ValueError("Enter a model ID of at most 200 characters.")
|
|
32
|
+
return model.strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def credentials(provider):
|
|
36
|
+
validate_provider(provider)
|
|
37
|
+
missing = [name for name in ("DATAMULE_API_KEY", KEY_VARIABLES[provider]) if not os.environ.get(name, "").strip()]
|
|
38
|
+
if missing:
|
|
39
|
+
raise ValueError("Set " + " and ".join(missing) + ".")
|
|
40
|
+
return os.environ[KEY_VARIABLES[provider]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _native_content(message, provider):
|
|
44
|
+
data = message.get("provider_data") or {}
|
|
45
|
+
return data.get("content") if data.get("provider") == provider else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_request(provider, model, key, messages, tools, tool_choice, reasoning_effort):
|
|
49
|
+
headers = {"Content-Type": "application/json"}
|
|
50
|
+
if provider in ("openrouter", "openai"):
|
|
51
|
+
history = []
|
|
52
|
+
for message in messages:
|
|
53
|
+
entry = {k: v for k, v in message.items() if k in ("role", "content", "tool_calls", "tool_call_id")}
|
|
54
|
+
if provider == "openrouter":
|
|
55
|
+
entry.update({k: v for k, v in message.items() if k in ("reasoning", "reasoning_details")})
|
|
56
|
+
history.append(entry)
|
|
57
|
+
payload = {"model": model, "messages": history}
|
|
58
|
+
if tools:
|
|
59
|
+
payload.update(tools=tools, tool_choice=tool_choice, parallel_tool_calls=False)
|
|
60
|
+
if provider == "openrouter":
|
|
61
|
+
payload.update(max_tokens=8192, reasoning={"effort": reasoning_effort or "medium", "exclude": False})
|
|
62
|
+
url = "https://openrouter.ai/api/v1/chat/completions"
|
|
63
|
+
else:
|
|
64
|
+
payload["max_completion_tokens"] = 8192
|
|
65
|
+
if reasoning_effort:
|
|
66
|
+
payload["reasoning_effort"] = reasoning_effort
|
|
67
|
+
url = "https://api.openai.com/v1/chat/completions"
|
|
68
|
+
headers["Authorization"] = f"Bearer {key}"
|
|
69
|
+
return url, headers, payload
|
|
70
|
+
|
|
71
|
+
system = "\n\n".join(message["content"] for message in messages if message["role"] == "system")
|
|
72
|
+
history, calls = [], {}
|
|
73
|
+
for message in messages:
|
|
74
|
+
role = message["role"]
|
|
75
|
+
if role == "system":
|
|
76
|
+
continue
|
|
77
|
+
for call in message.get("tool_calls", []):
|
|
78
|
+
calls[call["id"]] = call
|
|
79
|
+
native = _native_content(message, provider) if role == "assistant" else None
|
|
80
|
+
if provider == "anthropic":
|
|
81
|
+
target_role = "assistant" if role == "assistant" else "user"
|
|
82
|
+
if role == "tool":
|
|
83
|
+
content = [{"type": "tool_result", "tool_use_id": message["tool_call_id"], "content": message["content"]}]
|
|
84
|
+
elif native is not None:
|
|
85
|
+
content = native
|
|
86
|
+
else:
|
|
87
|
+
content = [{"type": "text", "text": message["content"]}] if message["content"] else []
|
|
88
|
+
content += [{"type": "tool_use", "id": call["id"], "name": call["function"]["name"],
|
|
89
|
+
"input": json.loads(call["function"]["arguments"])} for call in message.get("tool_calls", [])]
|
|
90
|
+
else:
|
|
91
|
+
target_role = "model" if role == "assistant" else "user"
|
|
92
|
+
if role == "tool":
|
|
93
|
+
call = calls[message["tool_call_id"]]
|
|
94
|
+
response = json.loads(message["content"])
|
|
95
|
+
result = {"name": call["function"]["name"], "response": response if isinstance(response, dict) else {"result": response}}
|
|
96
|
+
# Older Gemini versions don't assign function-call IDs.
|
|
97
|
+
if not call["id"].startswith("mulebot_"):
|
|
98
|
+
result["id"] = call["id"]
|
|
99
|
+
content = [{"functionResponse": result}]
|
|
100
|
+
elif native is not None:
|
|
101
|
+
content = native
|
|
102
|
+
else:
|
|
103
|
+
content = [{"text": message["content"]}] if message["content"] else []
|
|
104
|
+
content += [{"functionCall": {"name": call["function"]["name"], "args": json.loads(call["function"]["arguments"])}}
|
|
105
|
+
for call in message.get("tool_calls", [])]
|
|
106
|
+
field = "content" if provider == "anthropic" else "parts"
|
|
107
|
+
if content:
|
|
108
|
+
# Group parallel results into one user message, as required by Claude.
|
|
109
|
+
if history and history[-1]["role"] == target_role:
|
|
110
|
+
history[-1][field].extend(content)
|
|
111
|
+
else:
|
|
112
|
+
history.append({"role": target_role, field: list(content)})
|
|
113
|
+
|
|
114
|
+
if provider == "anthropic":
|
|
115
|
+
headers.update({"x-api-key": key, "anthropic-version": "2023-06-01"})
|
|
116
|
+
payload = {"model": model, "system": system, "messages": history, "max_tokens": 8192}
|
|
117
|
+
if tools:
|
|
118
|
+
payload["tools"] = [{"name": tool["function"]["name"], "description": tool["function"]["description"],
|
|
119
|
+
"input_schema": tool["function"]["parameters"]} for tool in tools]
|
|
120
|
+
payload["tool_choice"] = {"type": tool_choice}
|
|
121
|
+
if tool_choice == "auto":
|
|
122
|
+
payload["tool_choice"]["disable_parallel_tool_use"] = True
|
|
123
|
+
return "https://api.anthropic.com/v1/messages", headers, payload
|
|
124
|
+
|
|
125
|
+
headers["x-goog-api-key"] = key
|
|
126
|
+
payload = {"systemInstruction": {"parts": [{"text": system}]}, "contents": history,
|
|
127
|
+
"generationConfig": {"maxOutputTokens": 8192}}
|
|
128
|
+
if tools:
|
|
129
|
+
payload["tools"] = [{"functionDeclarations": [{"name": tool["function"]["name"],
|
|
130
|
+
"description": tool["function"]["description"],
|
|
131
|
+
"parametersJsonSchema": tool["function"]["parameters"]} for tool in tools]}]
|
|
132
|
+
payload["toolConfig"] = {"functionCallingConfig": {"mode": tool_choice.upper()}}
|
|
133
|
+
return f"https://generativelanguage.googleapis.com/v1beta/models/{quote(model, safe='')}:generateContent", headers, payload
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def normalize_response(provider, response):
|
|
137
|
+
if provider in ("openrouter", "openai") or response.get("error"):
|
|
138
|
+
return response
|
|
139
|
+
calls, text, reasoning = [], [], []
|
|
140
|
+
if provider == "anthropic":
|
|
141
|
+
content = response.get("content") or []
|
|
142
|
+
for block in content:
|
|
143
|
+
if block["type"] == "text":
|
|
144
|
+
text.append(block["text"])
|
|
145
|
+
elif block["type"] == "thinking":
|
|
146
|
+
reasoning.append(block["thinking"])
|
|
147
|
+
elif block["type"] == "tool_use":
|
|
148
|
+
calls.append({"id": block["id"], "type": "function", "function": {
|
|
149
|
+
"name": block["name"], "arguments": json.dumps(block["input"])}})
|
|
150
|
+
usage = dict(response.get("usage") or {})
|
|
151
|
+
usage.update(prompt_tokens=usage.get("input_tokens", 0) + usage.get("cache_creation_input_tokens", 0) + usage.get("cache_read_input_tokens", 0),
|
|
152
|
+
completion_tokens=usage.get("output_tokens", 0))
|
|
153
|
+
model, response_id = response.get("model"), response.get("id")
|
|
154
|
+
else:
|
|
155
|
+
candidate = (response.get("candidates") or [{}])[0]
|
|
156
|
+
content = (candidate.get("content") or {}).get("parts") or []
|
|
157
|
+
for part in content:
|
|
158
|
+
if "text" in part:
|
|
159
|
+
(reasoning if part.get("thought") else text).append(part["text"])
|
|
160
|
+
if "functionCall" in part:
|
|
161
|
+
call = part["functionCall"]
|
|
162
|
+
calls.append({"id": call.get("id") or "mulebot_" + uuid4().hex, "type": "function", "function": {
|
|
163
|
+
"name": call["name"], "arguments": json.dumps(call.get("args") or {})}})
|
|
164
|
+
usage = dict(response.get("usageMetadata") or {})
|
|
165
|
+
usage.update(prompt_tokens=usage.get("promptTokenCount", 0),
|
|
166
|
+
completion_tokens=usage.get("candidatesTokenCount", 0) + usage.get("thoughtsTokenCount", 0))
|
|
167
|
+
model, response_id = response.get("modelVersion"), response.get("responseId")
|
|
168
|
+
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
|
|
169
|
+
message = {"content": "\n\n".join(text), "tool_calls": calls, "reasoning": "\n\n".join(reasoning),
|
|
170
|
+
"provider_data": {"provider": provider, "content": content}}
|
|
171
|
+
return {"id": response_id, "model": model, "usage": usage, "choices": [{"message": message}]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import os
|
|
3
2
|
import sys
|
|
4
3
|
from contextlib import closing
|
|
5
4
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
6
5
|
|
|
7
|
-
from .agent import Agent
|
|
6
|
+
from .agent import Agent
|
|
7
|
+
from .providers import credentials, resolve_model
|
|
8
8
|
from .assets import resource
|
|
9
9
|
|
|
10
10
|
|
|
@@ -43,7 +43,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
43
43
|
"/static/style.css": ("static/style.css", "text/css; charset=utf-8"),
|
|
44
44
|
}
|
|
45
45
|
if self.path == "/api/config":
|
|
46
|
-
self.send_json(200, {"model": self.server.default_model})
|
|
46
|
+
self.send_json(200, {"provider": self.server.provider, "model": self.server.default_model})
|
|
47
47
|
elif self.path in assets:
|
|
48
48
|
name, content_type = assets[self.path]
|
|
49
49
|
body = resource("ui", *name.split("/")).read_text(encoding="utf-8")
|
|
@@ -75,7 +75,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
75
75
|
raise ValueError("Enter a message of at most 16,000 characters.")
|
|
76
76
|
model = body.get("model", self.server.default_model)
|
|
77
77
|
if not isinstance(model, str) or not model.strip() or len(model) > 200 or any(c.isspace() for c in model.strip()):
|
|
78
|
-
raise ValueError("Enter
|
|
78
|
+
raise ValueError("Enter a model ID of at most 200 characters.")
|
|
79
79
|
bot = self.server.agent_factory(model=model.strip())
|
|
80
80
|
bot.load_state(body.get("state"))
|
|
81
81
|
except (ValueError, UnicodeError) as exc:
|
|
@@ -94,15 +94,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
94
94
|
pass
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def create_server(port=8000, model=None, allowed_tables=None):
|
|
97
|
+
def create_server(provider, *, port=8000, model=None, allowed_tables=None):
|
|
98
|
+
default_model = resolve_model(provider, model)
|
|
99
|
+
credentials(provider)
|
|
98
100
|
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
99
|
-
server.
|
|
100
|
-
server.
|
|
101
|
+
server.provider = provider
|
|
102
|
+
server.default_model = default_model
|
|
103
|
+
server.agent_factory = lambda **settings: Agent(provider, allowed_tables=allowed_tables, **settings)
|
|
101
104
|
return server
|
|
102
105
|
|
|
103
106
|
|
|
104
|
-
def serve(port=8000, model=None, allowed_tables=None):
|
|
105
|
-
with create_server(port, model, allowed_tables) as server:
|
|
107
|
+
def serve(provider, *, port=8000, model=None, allowed_tables=None):
|
|
108
|
+
with create_server(provider, port=port, model=model, allowed_tables=allowed_tables) as server:
|
|
106
109
|
print(f"Mulebot: http://localhost:{server.server_port}", file=sys.stderr, flush=True)
|
|
107
110
|
try:
|
|
108
111
|
server.serve_forever()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import math
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
4
5
|
import sqlite3
|
|
5
6
|
import urllib.error
|
|
@@ -115,8 +116,10 @@ TOOL_DEFINITIONS = [
|
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
class Tools:
|
|
118
|
-
def __init__(self,
|
|
119
|
-
self.api_key =
|
|
119
|
+
def __init__(self, *, allowed_tables=None):
|
|
120
|
+
self.api_key = os.environ.get("DATAMULE_API_KEY", "")
|
|
121
|
+
if not self.api_key.strip():
|
|
122
|
+
raise ValueError("Set DATAMULE_API_KEY.")
|
|
120
123
|
self.allowed_tables = None if allowed_tables is None else {t.lower() for t in allowed_tables}
|
|
121
124
|
self.catalog = None
|
|
122
125
|
self.results = {}
|
|
@@ -25,6 +25,7 @@ const toolCalls = new Map();
|
|
|
25
25
|
const runningTools = new Set();
|
|
26
26
|
let busy = false;
|
|
27
27
|
let selectedModel = '';
|
|
28
|
+
let selectedProvider = '';
|
|
28
29
|
let agentState = null;
|
|
29
30
|
const CHAT_STORAGE = 'mulebot.chat';
|
|
30
31
|
let currentTools = null;
|
|
@@ -56,7 +57,7 @@ function debugHistory() {
|
|
|
56
57
|
|
|
57
58
|
function saveChat() {
|
|
58
59
|
try {
|
|
59
|
-
sessionStorage.setItem(CHAT_STORAGE, JSON.stringify({model: selectedModel, state: agentState, events: chatHistory}));
|
|
60
|
+
sessionStorage.setItem(CHAT_STORAGE, JSON.stringify({provider: selectedProvider, model: selectedModel, state: agentState, events: chatHistory}));
|
|
60
61
|
} catch {
|
|
61
62
|
settingsStatus.textContent = 'Chat is available in this tab but could not be saved for refresh.';
|
|
62
63
|
}
|
|
@@ -522,7 +523,7 @@ function saveModel(choice) {
|
|
|
522
523
|
if (!choice || choice === CUSTOM_MODEL) return;
|
|
523
524
|
if (choice === selectedModel) { showSelectedModel(); setBusy(false); return; }
|
|
524
525
|
if (choice.length > 200 || /\s/.test(choice)) {
|
|
525
|
-
settingsStatus.textContent = 'Enter
|
|
526
|
+
settingsStatus.textContent = 'Enter a model ID of at most 200 characters.';
|
|
526
527
|
return;
|
|
527
528
|
}
|
|
528
529
|
selectedModel = choice; showSelectedModel(); clearChat(); settingsStatus.textContent = 'New chat started';
|
|
@@ -586,10 +587,13 @@ setBusy(true);
|
|
|
586
587
|
fetch('/api/config').then(async response => {
|
|
587
588
|
if (!response.ok) throw new Error('Could not load chat.');
|
|
588
589
|
const config = await response.json(); selectedModel = config.model;
|
|
590
|
+
selectedProvider = config.provider;
|
|
591
|
+
if (selectedProvider !== 'openrouter') model.querySelector('optgroup')?.remove();
|
|
592
|
+
settingsStatus.textContent = `Provider: ${selectedProvider}`;
|
|
589
593
|
let saved = null;
|
|
590
594
|
try { saved = JSON.parse(sessionStorage.getItem(CHAT_STORAGE) || 'null'); }
|
|
591
595
|
catch { settingsStatus.textContent = 'Could not restore the saved chat.'; }
|
|
592
|
-
if (saved && typeof saved.model === 'string' && Array.isArray(saved.events)) {
|
|
596
|
+
if (saved && saved.provider === selectedProvider && typeof saved.model === 'string' && Array.isArray(saved.events)) {
|
|
593
597
|
selectedModel = saved.model; agentState = saved.state;
|
|
594
598
|
for (const event of saved.events) apply(event);
|
|
595
599
|
finishTools();
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
</select>
|
|
23
23
|
</label>
|
|
24
24
|
<form id="custom-model-form" class="custom-model" hidden>
|
|
25
|
-
<label for="custom-model">
|
|
25
|
+
<label for="custom-model">Model ID <input id="custom-model" placeholder="model-id" maxlength="200" required></label>
|
|
26
26
|
<button id="use-custom-model" type="submit">Use model</button>
|
|
27
27
|
<button id="cancel-custom-model" type="button">Cancel</button>
|
|
28
28
|
</form>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|