pythonclaw 0.6.0__py3-none-any.whl → 0.6.1__py3-none-any.whl
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.
- pythonclaw/channels/telegram_bot.py +203 -10
- pythonclaw/core/agent.py +78 -64
- pythonclaw/core/llm/anthropic_client.py +4 -1
- pythonclaw/core/llm/gemini_client.py +1 -0
- pythonclaw/core/llm/openai_compatible.py +5 -1
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/METADATA +1 -1
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/RECORD +11 -11
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/WHEEL +0 -0
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/entry_points.txt +0 -0
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {pythonclaw-0.6.0.dist-info → pythonclaw-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -32,6 +32,9 @@ from __future__ import annotations
|
|
|
32
32
|
import asyncio
|
|
33
33
|
import base64
|
|
34
34
|
import logging
|
|
35
|
+
import queue as _queue
|
|
36
|
+
import re
|
|
37
|
+
import time
|
|
35
38
|
from typing import TYPE_CHECKING
|
|
36
39
|
|
|
37
40
|
from telegram import BotCommand, ReactionTypeEmoji, Update
|
|
@@ -212,23 +215,27 @@ class TelegramBot:
|
|
|
212
215
|
except Exception:
|
|
213
216
|
pass
|
|
214
217
|
|
|
215
|
-
# Build multimodal input if photo is present
|
|
216
218
|
chat_input = user_text or ""
|
|
217
219
|
if has_photo:
|
|
218
220
|
chat_input = await self._build_image_input(
|
|
219
221
|
update, user_text or "What's in this image?"
|
|
220
222
|
)
|
|
221
223
|
|
|
224
|
+
token_queue: _queue.Queue[str] = _queue.Queue()
|
|
225
|
+
|
|
222
226
|
typing_task = asyncio.create_task(
|
|
223
227
|
self._keep_typing(update.message.chat_id)
|
|
224
228
|
)
|
|
225
229
|
try:
|
|
226
230
|
async with self._sm.acquire(sid):
|
|
227
231
|
loop = asyncio.get_event_loop()
|
|
228
|
-
|
|
232
|
+
future = loop.run_in_executor(
|
|
233
|
+
None, agent.chat_stream, chat_input, token_queue.put,
|
|
234
|
+
)
|
|
235
|
+
await self._flush_stream(update, token_queue, future)
|
|
229
236
|
except Exception as exc:
|
|
230
|
-
logger.exception("[Telegram] Agent
|
|
231
|
-
|
|
237
|
+
logger.exception("[Telegram] Agent error")
|
|
238
|
+
await update.message.reply_text(f"Sorry, something went wrong: {exc}")
|
|
232
239
|
finally:
|
|
233
240
|
typing_task.cancel()
|
|
234
241
|
|
|
@@ -237,8 +244,164 @@ class TelegramBot:
|
|
|
237
244
|
except Exception:
|
|
238
245
|
pass
|
|
239
246
|
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
# Max wall-clock time for a single agent invocation (seconds).
|
|
248
|
+
_AGENT_TIMEOUT = 180
|
|
249
|
+
|
|
250
|
+
async def _flush_stream(
|
|
251
|
+
self,
|
|
252
|
+
update: Update,
|
|
253
|
+
token_queue: "_queue.Queue[str]",
|
|
254
|
+
future: "asyncio.Future[str]",
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Progressively stream tokens to Telegram via edit-in-place.
|
|
257
|
+
|
|
258
|
+
Uses send-then-edit (like OpenClaw): one live message that gets
|
|
259
|
+
updated as tokens arrive (~1.5 s throttle). Tool-call markers
|
|
260
|
+
produce a short status line and start a fresh message.
|
|
261
|
+
|
|
262
|
+
Safeguards against hangs:
|
|
263
|
+
- **Heartbeat**: if no tokens arrive for 15 s, sends a "still
|
|
264
|
+
working" notification so the user knows the bot is alive.
|
|
265
|
+
- **Overall timeout**: after ``_AGENT_TIMEOUT`` seconds the
|
|
266
|
+
future is abandoned and a timeout message is sent.
|
|
267
|
+
"""
|
|
268
|
+
buf: list[str] = []
|
|
269
|
+
live_msg = None
|
|
270
|
+
live_text = ""
|
|
271
|
+
sent_any = False
|
|
272
|
+
THROTTLE = 1.5
|
|
273
|
+
HEARTBEAT_INTERVAL = 15.0
|
|
274
|
+
last_edit = time.monotonic()
|
|
275
|
+
last_token_time = time.monotonic()
|
|
276
|
+
start_time = time.monotonic()
|
|
277
|
+
heartbeat_sent = False
|
|
278
|
+
_MARKER = re.compile(r'`\[calling:\s*([^\]]+)\]`')
|
|
279
|
+
|
|
280
|
+
while not future.done():
|
|
281
|
+
# ── Overall timeout guard ─────────────────────────────────
|
|
282
|
+
if (time.monotonic() - start_time) > self._AGENT_TIMEOUT:
|
|
283
|
+
logger.warning(
|
|
284
|
+
"[Telegram] Agent timeout after %ds", self._AGENT_TIMEOUT,
|
|
285
|
+
)
|
|
286
|
+
try:
|
|
287
|
+
await update.message.reply_text(
|
|
288
|
+
"\u23f0 The operation timed out. "
|
|
289
|
+
"Please try a simpler request."
|
|
290
|
+
)
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
# ── Drain token queue ─────────────────────────────────────
|
|
296
|
+
drained = False
|
|
297
|
+
while True:
|
|
298
|
+
try:
|
|
299
|
+
buf.append(token_queue.get_nowait())
|
|
300
|
+
drained = True
|
|
301
|
+
last_token_time = time.monotonic()
|
|
302
|
+
heartbeat_sent = False
|
|
303
|
+
except _queue.Empty:
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
# ── Heartbeat: notify user during long silences ───────────
|
|
307
|
+
if (
|
|
308
|
+
not drained
|
|
309
|
+
and not heartbeat_sent
|
|
310
|
+
and (time.monotonic() - last_token_time) > HEARTBEAT_INTERVAL
|
|
311
|
+
):
|
|
312
|
+
try:
|
|
313
|
+
await update.message.reply_text(
|
|
314
|
+
"\u23f3 Still working\u2026"
|
|
315
|
+
)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
heartbeat_sent = True
|
|
319
|
+
|
|
320
|
+
if not drained:
|
|
321
|
+
await asyncio.sleep(0.3)
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
raw = "".join(buf)
|
|
325
|
+
now = time.monotonic()
|
|
326
|
+
|
|
327
|
+
# ── Tool-call marker → status line + new message ──────────
|
|
328
|
+
marker = _MARKER.search(raw)
|
|
329
|
+
if marker:
|
|
330
|
+
before = _clean_response(raw[:marker.start()])
|
|
331
|
+
if before and before != live_text:
|
|
332
|
+
try:
|
|
333
|
+
if live_msg:
|
|
334
|
+
await live_msg.edit_text(before[:4096])
|
|
335
|
+
else:
|
|
336
|
+
await update.message.reply_text(before[:4096])
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
live_msg = None
|
|
340
|
+
live_text = ""
|
|
341
|
+
tools = marker.group(1)
|
|
342
|
+
try:
|
|
343
|
+
await update.message.reply_text(
|
|
344
|
+
f"\U0001f527 {tools}\u2026"
|
|
345
|
+
)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
sent_any = True
|
|
349
|
+
buf = [raw[marker.end():].lstrip()]
|
|
350
|
+
last_edit = now
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
# ── Regular text → edit-in-place ──────────────────────────
|
|
354
|
+
text = _clean_response(raw)
|
|
355
|
+
if text and text != live_text and (now - last_edit) >= THROTTLE:
|
|
356
|
+
try:
|
|
357
|
+
if live_msg is None:
|
|
358
|
+
live_msg = await update.message.reply_text(
|
|
359
|
+
text[:4096],
|
|
360
|
+
)
|
|
361
|
+
live_text = text[:4096]
|
|
362
|
+
elif len(text) <= 4096:
|
|
363
|
+
await live_msg.edit_text(text)
|
|
364
|
+
live_text = text
|
|
365
|
+
else:
|
|
366
|
+
await live_msg.edit_text(text[:4096])
|
|
367
|
+
live_msg = None
|
|
368
|
+
live_text = ""
|
|
369
|
+
buf = [text[4096:]]
|
|
370
|
+
sent_any = True
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
last_edit = now
|
|
374
|
+
|
|
375
|
+
await asyncio.sleep(0.3)
|
|
376
|
+
|
|
377
|
+
# ── Final drain ───────────────────────────────────────────────
|
|
378
|
+
response = future.result()
|
|
379
|
+
while True:
|
|
380
|
+
try:
|
|
381
|
+
buf.append(token_queue.get_nowait())
|
|
382
|
+
except _queue.Empty:
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
remaining = _clean_response("".join(buf).strip())
|
|
386
|
+
if remaining and remaining != live_text:
|
|
387
|
+
try:
|
|
388
|
+
if live_msg and len(remaining) <= 4096:
|
|
389
|
+
await live_msg.edit_text(remaining)
|
|
390
|
+
elif live_msg:
|
|
391
|
+
await live_msg.edit_text(remaining[:4096])
|
|
392
|
+
for chunk in _split_message(remaining[4096:]):
|
|
393
|
+
await update.message.reply_text(chunk)
|
|
394
|
+
else:
|
|
395
|
+
for chunk in _split_message(remaining):
|
|
396
|
+
await update.message.reply_text(chunk)
|
|
397
|
+
sent_any = True
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
if not sent_any:
|
|
402
|
+
text = _clean_response(response or "(no response)")
|
|
403
|
+
for chunk in _split_message(text):
|
|
404
|
+
await update.message.reply_text(chunk)
|
|
242
405
|
|
|
243
406
|
async def _build_image_input(self, update: Update, caption: str) -> list:
|
|
244
407
|
"""Download photo and build a multimodal content array."""
|
|
@@ -330,14 +493,44 @@ class TelegramBot:
|
|
|
330
493
|
|
|
331
494
|
# ── Utility ───────────────────────────────────────────────────────────────────
|
|
332
495
|
|
|
496
|
+
_LEAKED_TOOL_RE = re.compile(
|
|
497
|
+
r'<\s*\|?\s*(?:DSML|antml)\s*\|\s*function_calls[^>]*>'
|
|
498
|
+
r'[\s\S]*?'
|
|
499
|
+
r'<\s*/\s*\|?\s*(?:DSML|antml)\s*\|\s*function_calls\s*>',
|
|
500
|
+
re.IGNORECASE,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _clean_response(text: str) -> str:
|
|
505
|
+
"""Strip leaked tool-call XML/DSML markup from LLM output."""
|
|
506
|
+
text = _LEAKED_TOOL_RE.sub('', text)
|
|
507
|
+
text = re.sub(r'\n{3,}', '\n\n', text)
|
|
508
|
+
return text.strip()
|
|
509
|
+
|
|
510
|
+
|
|
333
511
|
def _split_message(text: str, limit: int = 4096) -> list[str]:
|
|
334
|
-
"""Split
|
|
512
|
+
"""Split text into chunks respecting natural boundaries.
|
|
513
|
+
|
|
514
|
+
Tries paragraph breaks first, then newlines, then word boundaries,
|
|
515
|
+
and only falls back to a hard character cut as a last resort.
|
|
516
|
+
"""
|
|
335
517
|
if len(text) <= limit:
|
|
336
518
|
return [text]
|
|
337
|
-
chunks = []
|
|
519
|
+
chunks: list[str] = []
|
|
520
|
+
min_break = limit // 3
|
|
338
521
|
while text:
|
|
339
|
-
|
|
340
|
-
|
|
522
|
+
if len(text) <= limit:
|
|
523
|
+
chunks.append(text)
|
|
524
|
+
break
|
|
525
|
+
split_at = text.rfind('\n\n', min_break, limit)
|
|
526
|
+
if split_at < min_break:
|
|
527
|
+
split_at = text.rfind('\n', min_break, limit)
|
|
528
|
+
if split_at < min_break:
|
|
529
|
+
split_at = text.rfind(' ', min_break, limit)
|
|
530
|
+
if split_at < min_break:
|
|
531
|
+
split_at = limit
|
|
532
|
+
chunks.append(text[:split_at].rstrip())
|
|
533
|
+
text = text[split_at:].lstrip()
|
|
341
534
|
return chunks
|
|
342
535
|
|
|
343
536
|
|
pythonclaw/core/agent.py
CHANGED
|
@@ -24,6 +24,7 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import time
|
|
26
26
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
27
|
+
from concurrent.futures import TimeoutError as FuturesTimeout
|
|
27
28
|
from datetime import datetime
|
|
28
29
|
|
|
29
30
|
from .. import config
|
|
@@ -121,6 +122,7 @@ class Agent:
|
|
|
121
122
|
|
|
122
123
|
MAX_TOOL_ROUNDS = 8
|
|
123
124
|
MAX_PARALLEL_SKILLS = 5
|
|
125
|
+
TOOL_TIMEOUT = 90
|
|
124
126
|
|
|
125
127
|
def __init__(
|
|
126
128
|
self,
|
|
@@ -333,8 +335,11 @@ Always verify command output.
|
|
|
333
335
|
|
|
334
336
|
### Response Guidelines
|
|
335
337
|
- Answer the user's question directly and concisely.
|
|
338
|
+
- For complex multi-step tasks: share a **brief plan** first (2-4 bullet points), then work step by step. Report progress after each major step — don't wait until the end.
|
|
339
|
+
- Keep responses focused and concise — under 300 words when possible. Break long answers into short paragraphs.
|
|
336
340
|
- Do NOT mention what skills or tools you have available, unless explicitly asked.
|
|
337
341
|
- Do NOT list other things you can do at the end of your response.
|
|
342
|
+
- NEVER output tool calls as XML or text. Always use the function calling API.
|
|
338
343
|
"""
|
|
339
344
|
# ── Auto-inject memory context ────────────────────────────────────
|
|
340
345
|
boot_mem = self.memory.boot_context(max_chars=3000)
|
|
@@ -939,53 +944,53 @@ Don't repeat this if `bot_name` already exists in memory.
|
|
|
939
944
|
],
|
|
940
945
|
})
|
|
941
946
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
947
|
+
t0 = time.monotonic()
|
|
948
|
+
results: dict[str, str] = {}
|
|
949
|
+
with ThreadPoolExecutor(max_workers=min(len(tool_calls), 8)) as pool:
|
|
950
|
+
futures = {
|
|
951
|
+
pool.submit(self._execute_tool_call, tc): tc
|
|
952
|
+
for tc in tool_calls
|
|
953
|
+
}
|
|
954
|
+
for future in as_completed(futures, timeout=self.TOOL_TIMEOUT):
|
|
955
|
+
tc = futures[future]
|
|
956
|
+
try:
|
|
957
|
+
results[tc.id] = future.result()
|
|
958
|
+
except Exception as exc:
|
|
959
|
+
results[tc.id] = f"Error: {exc}"
|
|
960
|
+
for tc in tool_calls:
|
|
961
|
+
if tc.id not in results:
|
|
962
|
+
results[tc.id] = (
|
|
963
|
+
f"Error: tool '{tc.function.name}' timed out "
|
|
964
|
+
f"after {self.TOOL_TIMEOUT}s"
|
|
965
|
+
)
|
|
966
|
+
_log_detail({
|
|
967
|
+
"event": "tool_results",
|
|
968
|
+
"round": tool_rounds,
|
|
969
|
+
"count": len(tool_calls),
|
|
970
|
+
"elapsed_ms": int((time.monotonic() - t0) * 1000),
|
|
971
|
+
"tools": [tc.function.name for tc in tool_calls],
|
|
972
|
+
})
|
|
973
|
+
for tc in tool_calls:
|
|
952
974
|
self.messages.append({
|
|
953
975
|
"role": "tool",
|
|
954
|
-
"tool_call_id":
|
|
955
|
-
"content":
|
|
976
|
+
"tool_call_id": tc.id,
|
|
977
|
+
"content": results[tc.id],
|
|
956
978
|
})
|
|
957
|
-
else:
|
|
958
|
-
t0 = time.monotonic()
|
|
959
|
-
results: dict[str, str] = {}
|
|
960
|
-
with ThreadPoolExecutor(max_workers=min(len(tool_calls), 8)) as pool:
|
|
961
|
-
futures = {
|
|
962
|
-
pool.submit(self._execute_tool_call, tc): tc
|
|
963
|
-
for tc in tool_calls
|
|
964
|
-
}
|
|
965
|
-
for future in as_completed(futures):
|
|
966
|
-
tc = futures[future]
|
|
967
|
-
try:
|
|
968
|
-
results[tc.id] = future.result()
|
|
969
|
-
except Exception as exc:
|
|
970
|
-
results[tc.id] = f"Error: {exc}"
|
|
971
|
-
_log_detail({
|
|
972
|
-
"event": "tool_results_parallel",
|
|
973
|
-
"round": tool_rounds,
|
|
974
|
-
"count": len(tool_calls),
|
|
975
|
-
"elapsed_ms": int((time.monotonic() - t0) * 1000),
|
|
976
|
-
"tools": [tc.function.name for tc in tool_calls],
|
|
977
|
-
})
|
|
978
|
-
for tc in tool_calls:
|
|
979
|
-
self.messages.append({
|
|
980
|
-
"role": "tool",
|
|
981
|
-
"tool_call_id": tc.id,
|
|
982
|
-
"content": results[tc.id],
|
|
983
|
-
})
|
|
984
979
|
|
|
985
980
|
for injection in self.pending_injections:
|
|
986
981
|
self.messages.append({"role": "system", "content": injection})
|
|
987
982
|
self.pending_injections = []
|
|
988
983
|
|
|
984
|
+
except FuturesTimeout:
|
|
985
|
+
logger.warning("Tool execution timed out at round %d", tool_rounds)
|
|
986
|
+
for tc in tool_calls:
|
|
987
|
+
if tc.id not in results:
|
|
988
|
+
self.messages.append({
|
|
989
|
+
"role": "tool",
|
|
990
|
+
"tool_call_id": tc.id,
|
|
991
|
+
"content": f"Error: timed out after {self.TOOL_TIMEOUT}s",
|
|
992
|
+
})
|
|
993
|
+
continue
|
|
989
994
|
except Exception as exc:
|
|
990
995
|
logger.exception("Critical error in Agent.chat()")
|
|
991
996
|
return f"Error: {exc}"
|
|
@@ -1087,34 +1092,33 @@ Don't repeat this if `bot_name` already exists in memory.
|
|
|
1087
1092
|
names = ", ".join(tc.function.name for tc in tool_calls)
|
|
1088
1093
|
on_token(f"\n\n`[calling: {names}]`\n\n")
|
|
1089
1094
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1095
|
+
results: dict[str, str] = {}
|
|
1096
|
+
with ThreadPoolExecutor(
|
|
1097
|
+
max_workers=min(len(tool_calls), 8)
|
|
1098
|
+
) as pool:
|
|
1099
|
+
futures = {
|
|
1100
|
+
pool.submit(self._execute_tool_call, tc): tc
|
|
1101
|
+
for tc in tool_calls
|
|
1102
|
+
}
|
|
1103
|
+
for future in as_completed(
|
|
1104
|
+
futures, timeout=self.TOOL_TIMEOUT
|
|
1105
|
+
):
|
|
1106
|
+
tc = futures[future]
|
|
1107
|
+
try:
|
|
1108
|
+
results[tc.id] = future.result()
|
|
1109
|
+
except Exception as exc:
|
|
1110
|
+
results[tc.id] = f"Error: {exc}"
|
|
1111
|
+
for tc in tool_calls:
|
|
1112
|
+
if tc.id not in results:
|
|
1113
|
+
results[tc.id] = (
|
|
1114
|
+
f"Error: tool '{tc.function.name}' timed out "
|
|
1115
|
+
f"after {self.TOOL_TIMEOUT}s"
|
|
1116
|
+
)
|
|
1092
1117
|
self.messages.append({
|
|
1093
1118
|
"role": "tool",
|
|
1094
|
-
"tool_call_id":
|
|
1095
|
-
"content":
|
|
1119
|
+
"tool_call_id": tc.id,
|
|
1120
|
+
"content": results[tc.id],
|
|
1096
1121
|
})
|
|
1097
|
-
else:
|
|
1098
|
-
results: dict[str, str] = {}
|
|
1099
|
-
with ThreadPoolExecutor(
|
|
1100
|
-
max_workers=min(len(tool_calls), 8)
|
|
1101
|
-
) as pool:
|
|
1102
|
-
futures = {
|
|
1103
|
-
pool.submit(self._execute_tool_call, tc): tc
|
|
1104
|
-
for tc in tool_calls
|
|
1105
|
-
}
|
|
1106
|
-
for future in as_completed(futures):
|
|
1107
|
-
tc = futures[future]
|
|
1108
|
-
try:
|
|
1109
|
-
results[tc.id] = future.result()
|
|
1110
|
-
except Exception as exc:
|
|
1111
|
-
results[tc.id] = f"Error: {exc}"
|
|
1112
|
-
for tc in tool_calls:
|
|
1113
|
-
self.messages.append({
|
|
1114
|
-
"role": "tool",
|
|
1115
|
-
"tool_call_id": tc.id,
|
|
1116
|
-
"content": results[tc.id],
|
|
1117
|
-
})
|
|
1118
1122
|
|
|
1119
1123
|
for injection in self.pending_injections:
|
|
1120
1124
|
self.messages.append(
|
|
@@ -1122,6 +1126,16 @@ Don't repeat this if `bot_name` already exists in memory.
|
|
|
1122
1126
|
)
|
|
1123
1127
|
self.pending_injections = []
|
|
1124
1128
|
|
|
1129
|
+
except FuturesTimeout:
|
|
1130
|
+
logger.warning("Tool execution timed out in stream round %d", tool_rounds)
|
|
1131
|
+
for tc in tool_calls:
|
|
1132
|
+
if tc.id not in results:
|
|
1133
|
+
self.messages.append({
|
|
1134
|
+
"role": "tool",
|
|
1135
|
+
"tool_call_id": tc.id,
|
|
1136
|
+
"content": f"Error: timed out after {self.TOOL_TIMEOUT}s",
|
|
1137
|
+
})
|
|
1138
|
+
continue
|
|
1125
1139
|
except Exception as exc:
|
|
1126
1140
|
logger.exception("Critical error in Agent.chat_stream()")
|
|
1127
1141
|
return f"Error: {exc}"
|
|
@@ -27,7 +27,10 @@ class AnthropicProvider(LLMProvider):
|
|
|
27
27
|
supports_images = True
|
|
28
28
|
|
|
29
29
|
def __init__(self, api_key: str, model_name: str = "claude-sonnet-4-20250514"):
|
|
30
|
-
self.client = anthropic.Anthropic(
|
|
30
|
+
self.client = anthropic.Anthropic(
|
|
31
|
+
api_key=api_key,
|
|
32
|
+
timeout=120.0,
|
|
33
|
+
)
|
|
31
34
|
self.model_name = model_name
|
|
32
35
|
self._auth_type = (
|
|
33
36
|
"setup-token" if not api_key.startswith("sk-ant-") else "api-key"
|
|
@@ -19,7 +19,11 @@ class OpenAICompatibleProvider(LLMProvider):
|
|
|
19
19
|
"""Thin wrapper around the OpenAI SDK for chat completions."""
|
|
20
20
|
|
|
21
21
|
def __init__(self, api_key: str, base_url: str, model_name: str) -> None:
|
|
22
|
-
self.client = OpenAI(
|
|
22
|
+
self.client = OpenAI(
|
|
23
|
+
api_key=api_key,
|
|
24
|
+
base_url=base_url,
|
|
25
|
+
timeout=120.0,
|
|
26
|
+
)
|
|
23
27
|
self.model_name = model_name
|
|
24
28
|
|
|
25
29
|
def chat(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonclaw
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support.
|
|
5
5
|
Author-email: Eric Wang <wangchen2007915@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,10 +8,10 @@ pythonclaw/onboard.py,sha256=X6ViAduToi9P9J_WdWm9mKQtTb48IwGj5DiqS4jjt0Y,13921
|
|
|
8
8
|
pythonclaw/server.py,sha256=zUV09uNTmzK597swGwt45gdmVxuHPsF7ogVV0DHBIhA,4521
|
|
9
9
|
pythonclaw/session_manager.py,sha256=LKRolNa2i3evb6Ps1zRbajlk4AfvujbR1iPlhfAMBj8,5981
|
|
10
10
|
pythonclaw/channels/discord_bot.py,sha256=95IJcBJlcnOSbsg0LILq6uBYfhdpb-iLLlRw_41Xz7U,11744
|
|
11
|
-
pythonclaw/channels/telegram_bot.py,sha256=
|
|
11
|
+
pythonclaw/channels/telegram_bot.py,sha256=Yb2kKQy9V1VAJWJzHNvCtGAxircxi4h7fImh7Qg1A3U,22183
|
|
12
12
|
pythonclaw/channels/whatsapp_bot.py,sha256=60n6W3ONEIKAdpmI6gCS9RWf5KkLtMUU4J5NJH8vQEY,10650
|
|
13
13
|
pythonclaw/core/__init__.py,sha256=G5LCqUcCIcYYbMv6SreqS-kj8T9n-IvBAhHEG7wDF5w,661
|
|
14
|
-
pythonclaw/core/agent.py,sha256=
|
|
14
|
+
pythonclaw/core/agent.py,sha256=4YfUQ5VzKdzf6CACoXIYS3XNb39eGeuZtR59j5HtEF8,50078
|
|
15
15
|
pythonclaw/core/compaction.py,sha256=b3zrqwBhPmsmQdfRjrvKK6j0hcfNLpiRrZ8qFxY1h1U,8966
|
|
16
16
|
pythonclaw/core/persistent_agent.py,sha256=nnY1vaZFsn0Wd_MQ27wbG7sRjO1v2ZxbwXjnJGKsBZc,4932
|
|
17
17
|
pythonclaw/core/session_store.py,sha256=V44wMBbMpbDCh1aiCa5lb0_iXrKA_7VrR1rOHGGZYhs,9458
|
|
@@ -20,10 +20,10 @@ pythonclaw/core/skillhub.py,sha256=3MGJ81DFcgcVDCD_Wvf5vHJhcDLZf6IKOhzGzaHkPPQ,1
|
|
|
20
20
|
pythonclaw/core/tools.py,sha256=e3ZZnZ5uZt1bj30IBMJP9ZAwhXUEs-3F_q1tmE2Uk90,23205
|
|
21
21
|
pythonclaw/core/utils.py,sha256=Ih_ZYnulGlxctdyVy4oKknjvkwFS6ZHcdrznIFIAwxo,1919
|
|
22
22
|
pythonclaw/core/knowledge/rag.py,sha256=_6GKs8ZFirMQhOeT-CAJBkwLcPkEz7Og-gWKMfUezDw,2895
|
|
23
|
-
pythonclaw/core/llm/anthropic_client.py,sha256=
|
|
23
|
+
pythonclaw/core/llm/anthropic_client.py,sha256=w6mXlpxvdmulZMD8_ocC8cufDON-RjLIvslWiPb-GnM,11797
|
|
24
24
|
pythonclaw/core/llm/base.py,sha256=y1muHBuK14rvzWlXmoSf6ahz6Xi0BojpnDUTRhaD3pI,1683
|
|
25
|
-
pythonclaw/core/llm/gemini_client.py,sha256=
|
|
26
|
-
pythonclaw/core/llm/openai_compatible.py,sha256=
|
|
25
|
+
pythonclaw/core/llm/gemini_client.py,sha256=Oarzq12YnuMdivAYLLsVmMUTvNCmXXxHfitbUAebYu8,7218
|
|
26
|
+
pythonclaw/core/llm/openai_compatible.py,sha256=4FK1OB93tB_ARs_0vdvBADtP3cuNReixNWf6PUkfCys,3567
|
|
27
27
|
pythonclaw/core/llm/response.py,sha256=hNCsi0aV1ffXsFuDNnBpRp96cFtVDfX_XEC34QZoykc,1223
|
|
28
28
|
pythonclaw/core/memory/manager.py,sha256=JzNT6CGVRmmIqbOflRzF7HxSfPfI5jLu8tmF6-91ZVA,8945
|
|
29
29
|
pythonclaw/core/memory/storage.py,sha256=mHDN8yCVUZ5srOwYWDNjUhbELXka-X8zSexFWEBUB1M,9119
|
|
@@ -112,9 +112,9 @@ pythonclaw/web/app.py,sha256=uudrxieo5oGwhQUBLzkmn6GU6SnR4VKlRYOg1bFAYQg,32208
|
|
|
112
112
|
pythonclaw/web/static/favicon.png,sha256=zJA13uE8mSe6lOlR5NyAhiOmnZkfv7ZlBbSBNCH7iTM,2557
|
|
113
113
|
pythonclaw/web/static/index.html,sha256=wU4Lw0NcenS0i0HsJS6a6ceFefDxpRsUQnCXVUXYWVU,93734
|
|
114
114
|
pythonclaw/web/static/logo.png,sha256=h7v0HHllD23FtmCL2UvjjTDt0UgqKjGy5jOhI3rb7lM,28359
|
|
115
|
-
pythonclaw-0.6.
|
|
116
|
-
pythonclaw-0.6.
|
|
117
|
-
pythonclaw-0.6.
|
|
118
|
-
pythonclaw-0.6.
|
|
119
|
-
pythonclaw-0.6.
|
|
120
|
-
pythonclaw-0.6.
|
|
115
|
+
pythonclaw-0.6.1.dist-info/licenses/LICENSE,sha256=wbYsm5Ofe8cnxHgWSnSG1vUJDNiY1DIeTyxHSbo1HqM,1066
|
|
116
|
+
pythonclaw-0.6.1.dist-info/METADATA,sha256=n3lgHkJkHYMEk70VecYW9rgsP1gdwJukIdQeRtkooPE,14919
|
|
117
|
+
pythonclaw-0.6.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
118
|
+
pythonclaw-0.6.1.dist-info/entry_points.txt,sha256=4uGCuBw-id_22IRdkoxPVOOcwgiPX5lNEpD1XKQWE4I,52
|
|
119
|
+
pythonclaw-0.6.1.dist-info/top_level.txt,sha256=S_lM2VH3gP3UeZbSWHXIrBOCNtoqn5pk491IAzgsV7M,11
|
|
120
|
+
pythonclaw-0.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|