squidbot 0.1.0__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.
- squidbot/__init__.py +5 -0
- squidbot/agent.py +263 -0
- squidbot/channels.py +271 -0
- squidbot/character.py +83 -0
- squidbot/client.py +318 -0
- squidbot/config.py +148 -0
- squidbot/daemon.py +310 -0
- squidbot/lanes.py +41 -0
- squidbot/main.py +157 -0
- squidbot/memory_db.py +706 -0
- squidbot/playwright_check.py +233 -0
- squidbot/plugins/__init__.py +47 -0
- squidbot/plugins/base.py +96 -0
- squidbot/plugins/hooks.py +416 -0
- squidbot/plugins/loader.py +248 -0
- squidbot/plugins/web3_plugin.py +407 -0
- squidbot/scheduler.py +214 -0
- squidbot/server.py +487 -0
- squidbot/session.py +609 -0
- squidbot/skills.py +141 -0
- squidbot/skills_template/reminder/SKILL.md +13 -0
- squidbot/skills_template/search/SKILL.md +11 -0
- squidbot/skills_template/summarize/SKILL.md +14 -0
- squidbot/tools/__init__.py +100 -0
- squidbot/tools/base.py +42 -0
- squidbot/tools/browser.py +311 -0
- squidbot/tools/coding.py +599 -0
- squidbot/tools/cron.py +218 -0
- squidbot/tools/memory_tool.py +152 -0
- squidbot/tools/web_search.py +50 -0
- squidbot-0.1.0.dist-info/METADATA +542 -0
- squidbot-0.1.0.dist-info/RECORD +34 -0
- squidbot-0.1.0.dist-info/WHEEL +4 -0
- squidbot-0.1.0.dist-info/entry_points.txt +4 -0
squidbot/client.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SquidBot Client
|
|
4
|
+
|
|
5
|
+
Terminal-based chat client that connects to the local SquidBot server.
|
|
6
|
+
Features: async operations, input history, loading animation, notifications.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import readline
|
|
12
|
+
import signal
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .config import DATA_DIR, SQUID_PORT
|
|
18
|
+
|
|
19
|
+
# Server configuration
|
|
20
|
+
SERVER_HOST = "127.0.0.1"
|
|
21
|
+
SERVER_PORT = SQUID_PORT
|
|
22
|
+
|
|
23
|
+
# History file
|
|
24
|
+
HISTORY_FILE = DATA_DIR / "client_history"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Spinner:
|
|
28
|
+
"""Animated spinner for loading indication with elapsed time."""
|
|
29
|
+
|
|
30
|
+
FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str = "Thinking"):
|
|
33
|
+
self.message = message
|
|
34
|
+
self.running = False
|
|
35
|
+
self.task: asyncio.Task | None = None
|
|
36
|
+
self.start_time: float = 0
|
|
37
|
+
self.elapsed: float = 0
|
|
38
|
+
|
|
39
|
+
async def _animate(self):
|
|
40
|
+
"""Run the animation loop."""
|
|
41
|
+
idx = 0
|
|
42
|
+
while self.running:
|
|
43
|
+
frame = self.FRAMES[idx % len(self.FRAMES)]
|
|
44
|
+
self.elapsed = time.time() - self.start_time
|
|
45
|
+
print(
|
|
46
|
+
f"\r{frame} {self.message}... ({self.elapsed:.1f}s)", end="", flush=True
|
|
47
|
+
)
|
|
48
|
+
idx += 1
|
|
49
|
+
await asyncio.sleep(0.1)
|
|
50
|
+
|
|
51
|
+
async def start(self):
|
|
52
|
+
"""Start the spinner."""
|
|
53
|
+
self.start_time = time.time()
|
|
54
|
+
self.running = True
|
|
55
|
+
self.task = asyncio.create_task(self._animate())
|
|
56
|
+
|
|
57
|
+
async def stop(self) -> float:
|
|
58
|
+
"""Stop the spinner and clear the line. Returns elapsed time."""
|
|
59
|
+
self.running = False
|
|
60
|
+
self.elapsed = time.time() - self.start_time
|
|
61
|
+
if self.task:
|
|
62
|
+
self.task.cancel()
|
|
63
|
+
try:
|
|
64
|
+
await self.task
|
|
65
|
+
except asyncio.CancelledError:
|
|
66
|
+
pass
|
|
67
|
+
# Clear the spinner line
|
|
68
|
+
print("\r" + " " * 50 + "\r", end="", flush=True)
|
|
69
|
+
return self.elapsed
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class InputHistory:
|
|
73
|
+
"""Manage input history with readline."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, history_file: Path):
|
|
76
|
+
self.history_file = history_file
|
|
77
|
+
|
|
78
|
+
async def load(self):
|
|
79
|
+
"""Load history from file."""
|
|
80
|
+
try:
|
|
81
|
+
if self.history_file.exists():
|
|
82
|
+
readline.read_history_file(str(self.history_file))
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
async def save(self):
|
|
87
|
+
"""Save history to file."""
|
|
88
|
+
try:
|
|
89
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
readline.set_history_length(1000)
|
|
91
|
+
readline.write_history_file(str(self.history_file))
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
async def setup(self):
|
|
96
|
+
"""Setup readline for history navigation."""
|
|
97
|
+
readline.parse_and_bind(r'"\e[A": previous-history')
|
|
98
|
+
readline.parse_and_bind(r'"\e[B": next-history')
|
|
99
|
+
readline.parse_and_bind(r'"\e[C": forward-char')
|
|
100
|
+
readline.parse_and_bind(r'"\e[D": backward-char')
|
|
101
|
+
readline.parse_and_bind(r'"\e[1~": beginning-of-line')
|
|
102
|
+
readline.parse_and_bind(r'"\e[4~": end-of-line')
|
|
103
|
+
readline.parse_and_bind(r'"\e[3~": delete-char')
|
|
104
|
+
await self.load()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SquidBotClient:
|
|
108
|
+
"""Async chat client for SquidBot."""
|
|
109
|
+
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.reader: asyncio.StreamReader | None = None
|
|
112
|
+
self.writer: asyncio.StreamWriter | None = None
|
|
113
|
+
self.running = True
|
|
114
|
+
self.history = InputHistory(HISTORY_FILE)
|
|
115
|
+
self.spinner = Spinner("Thinking")
|
|
116
|
+
self.pending_notifications: list[str] = []
|
|
117
|
+
self.response_queue: asyncio.Queue = asyncio.Queue()
|
|
118
|
+
self.reader_task: asyncio.Task | None = None
|
|
119
|
+
|
|
120
|
+
async def connect(self) -> bool:
|
|
121
|
+
"""Connect to the server."""
|
|
122
|
+
try:
|
|
123
|
+
self.reader, self.writer = await asyncio.open_connection(
|
|
124
|
+
SERVER_HOST, SERVER_PORT
|
|
125
|
+
)
|
|
126
|
+
return True
|
|
127
|
+
except ConnectionRefusedError:
|
|
128
|
+
print(f"Error: Cannot connect to server at {SERVER_HOST}:{SERVER_PORT}")
|
|
129
|
+
print("Make sure the server is running: make start")
|
|
130
|
+
print(f"(Port configured via SQUID_PORT in .env)")
|
|
131
|
+
return False
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Error connecting: {e}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
async def send_request(self, message: str, command: str = "chat") -> None:
|
|
137
|
+
"""Send a request to the server."""
|
|
138
|
+
if not self.writer:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
request = {"command": command, "message": message}
|
|
142
|
+
data = json.dumps(request) + "\n"
|
|
143
|
+
self.writer.write(data.encode())
|
|
144
|
+
await self.writer.drain()
|
|
145
|
+
|
|
146
|
+
async def read_responses(self):
|
|
147
|
+
"""Single reader task that dispatches responses to the right handler."""
|
|
148
|
+
while self.running and self.reader:
|
|
149
|
+
try:
|
|
150
|
+
data = await self.reader.readline()
|
|
151
|
+
if not data:
|
|
152
|
+
# Connection closed
|
|
153
|
+
await self.response_queue.put(None)
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
response = json.loads(data.decode().strip())
|
|
157
|
+
|
|
158
|
+
if response.get("status") == "notification":
|
|
159
|
+
# Handle notification immediately
|
|
160
|
+
msg = response.get("response", "")
|
|
161
|
+
print(f"\n\n📢 [Scheduled Task]\n{msg}\n")
|
|
162
|
+
print("You: ", end="", flush=True)
|
|
163
|
+
else:
|
|
164
|
+
# Queue response for chat handler
|
|
165
|
+
await self.response_queue.put(response)
|
|
166
|
+
|
|
167
|
+
except asyncio.CancelledError:
|
|
168
|
+
break
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
continue
|
|
171
|
+
except Exception:
|
|
172
|
+
await self.response_queue.put(None)
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
async def get_response(self, timeout: float = 120.0) -> dict | None:
|
|
176
|
+
"""Get a response from the queue with timeout."""
|
|
177
|
+
try:
|
|
178
|
+
return await asyncio.wait_for(self.response_queue.get(), timeout=timeout)
|
|
179
|
+
except asyncio.TimeoutError:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
async def chat(self, user_input: str) -> tuple[str | None, float]:
|
|
183
|
+
"""Send chat message with loading animation."""
|
|
184
|
+
await self.spinner.start()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
await self.send_request(user_input, "chat")
|
|
188
|
+
|
|
189
|
+
# Wait for response from queue
|
|
190
|
+
response = await self.get_response(timeout=120.0)
|
|
191
|
+
if response is None:
|
|
192
|
+
return None, self.spinner.elapsed
|
|
193
|
+
|
|
194
|
+
return response.get("response", ""), self.spinner.elapsed
|
|
195
|
+
finally:
|
|
196
|
+
await self.spinner.stop()
|
|
197
|
+
|
|
198
|
+
async def close(self):
|
|
199
|
+
"""Close the connection."""
|
|
200
|
+
if self.reader_task:
|
|
201
|
+
self.reader_task.cancel()
|
|
202
|
+
try:
|
|
203
|
+
await self.reader_task
|
|
204
|
+
except asyncio.CancelledError:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
if self.writer:
|
|
208
|
+
self.writer.close()
|
|
209
|
+
await self.writer.wait_closed()
|
|
210
|
+
|
|
211
|
+
async def get_input(self, prompt: str) -> str:
|
|
212
|
+
"""Get user input asynchronously."""
|
|
213
|
+
loop = asyncio.get_event_loop()
|
|
214
|
+
return await loop.run_in_executor(None, lambda: input(prompt))
|
|
215
|
+
|
|
216
|
+
async def run(self):
|
|
217
|
+
"""Run the interactive chat loop."""
|
|
218
|
+
print("=" * 60)
|
|
219
|
+
print(" SquidBot Client")
|
|
220
|
+
print("=" * 60)
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
if not await self.connect():
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Start the single reader task
|
|
227
|
+
self.reader_task = asyncio.create_task(self.read_responses())
|
|
228
|
+
|
|
229
|
+
# Verify connection
|
|
230
|
+
await self.send_request("", "ping")
|
|
231
|
+
response = await self.get_response(timeout=5.0)
|
|
232
|
+
if not response or response.get("response") != "pong":
|
|
233
|
+
print("Error: Server not responding correctly")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# Setup history
|
|
237
|
+
await self.history.setup()
|
|
238
|
+
|
|
239
|
+
print(f"Connected to SquidBot at {SERVER_HOST}:{SERVER_PORT}")
|
|
240
|
+
print()
|
|
241
|
+
print("Commands:")
|
|
242
|
+
print(" /clear - Clear conversation history")
|
|
243
|
+
print(" /quit - Exit the client")
|
|
244
|
+
print()
|
|
245
|
+
print("Tips:")
|
|
246
|
+
print(" - Use ↑/↓ arrows to navigate input history")
|
|
247
|
+
print(" - Scheduled tasks will appear automatically")
|
|
248
|
+
print()
|
|
249
|
+
print("-" * 60)
|
|
250
|
+
|
|
251
|
+
while self.running:
|
|
252
|
+
try:
|
|
253
|
+
# Get user input
|
|
254
|
+
user_input = await self.get_input("\nYou: ")
|
|
255
|
+
|
|
256
|
+
if not user_input.strip():
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Handle commands
|
|
260
|
+
cmd = user_input.strip().lower()
|
|
261
|
+
if cmd in ["/quit", "/exit", "/q"]:
|
|
262
|
+
print("Goodbye!")
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
if cmd == "/clear":
|
|
266
|
+
await self.send_request("", "clear")
|
|
267
|
+
response = await self.get_response(timeout=5.0)
|
|
268
|
+
if response:
|
|
269
|
+
print(f"\n{response.get('response', '')}")
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Send chat message with animation
|
|
273
|
+
response, elapsed = await self.chat(user_input)
|
|
274
|
+
|
|
275
|
+
if response is None:
|
|
276
|
+
print("Error: Lost connection to server")
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
print(f"SquidBot: {response}")
|
|
280
|
+
print(f" ({elapsed:.1f}s)")
|
|
281
|
+
|
|
282
|
+
except EOFError:
|
|
283
|
+
print("\nGoodbye!")
|
|
284
|
+
break
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
print("\nGoodbye!")
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
# Cleanup
|
|
290
|
+
self.running = False
|
|
291
|
+
await self.history.save()
|
|
292
|
+
await self.close()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def async_main():
|
|
296
|
+
"""Async main entry point."""
|
|
297
|
+
client = SquidBotClient()
|
|
298
|
+
|
|
299
|
+
def signal_handler():
|
|
300
|
+
client.running = False
|
|
301
|
+
|
|
302
|
+
loop = asyncio.get_event_loop()
|
|
303
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
304
|
+
try:
|
|
305
|
+
loop.add_signal_handler(sig, signal_handler)
|
|
306
|
+
except NotImplementedError:
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
await client.run()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main():
|
|
313
|
+
"""Main entry point."""
|
|
314
|
+
asyncio.run(async_main())
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
main()
|
squidbot/config.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Configuration loader from environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
load_dotenv(override=True) # Override existing env vars with .env values
|
|
9
|
+
|
|
10
|
+
# Required
|
|
11
|
+
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
12
|
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
|
13
|
+
|
|
14
|
+
# Optional
|
|
15
|
+
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o")
|
|
16
|
+
HEARTBEAT_INTERVAL_MINUTES = int(os.environ.get("HEARTBEAT_INTERVAL_MINUTES", "30"))
|
|
17
|
+
SQUID_PORT = int(os.environ.get("SQUID_PORT", "7777"))
|
|
18
|
+
|
|
19
|
+
# Paths - Use SQUIDBOT_HOME env var or default to ~/.squidbot
|
|
20
|
+
DATA_DIR = Path(os.environ.get("SQUIDBOT_HOME", Path.home() / ".squidbot"))
|
|
21
|
+
MEMORY_FILE = DATA_DIR / "memory.json"
|
|
22
|
+
CRON_FILE = DATA_DIR / "cron_jobs.json"
|
|
23
|
+
CHARACTER_FILE = DATA_DIR / "CHARACTER.md"
|
|
24
|
+
SKILLS_DIR = DATA_DIR / "skills"
|
|
25
|
+
CODING_DIR = DATA_DIR / "coding"
|
|
26
|
+
SESSIONS_DIR = DATA_DIR / "sessions"
|
|
27
|
+
|
|
28
|
+
# Default CHARACTER.md content
|
|
29
|
+
DEFAULT_CHARACTER = """# Character Definition
|
|
30
|
+
|
|
31
|
+
You are a helpful AI assistant with the following traits:
|
|
32
|
+
|
|
33
|
+
## Personality
|
|
34
|
+
- Friendly and approachable
|
|
35
|
+
- Patient and thorough
|
|
36
|
+
- Honest about limitations
|
|
37
|
+
|
|
38
|
+
## Communication Style
|
|
39
|
+
- Clear and concise responses
|
|
40
|
+
- Use examples when helpful
|
|
41
|
+
- Ask clarifying questions when needed
|
|
42
|
+
|
|
43
|
+
## Guidelines
|
|
44
|
+
- Always be helpful and respectful
|
|
45
|
+
- Admit when you don't know something
|
|
46
|
+
- Provide accurate information
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Default skill template
|
|
50
|
+
DEFAULT_SKILL_SEARCH = """---
|
|
51
|
+
name: search
|
|
52
|
+
description: Search the web for information
|
|
53
|
+
---
|
|
54
|
+
When user asks a question requiring current information:
|
|
55
|
+
|
|
56
|
+
1. Use `web_search` tool with a clear query
|
|
57
|
+
2. Review the results
|
|
58
|
+
3. Synthesize a helpful answer with sources
|
|
59
|
+
|
|
60
|
+
Always cite sources when providing factual information.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
DEFAULT_SKILL_REMINDER = """---
|
|
64
|
+
name: reminder
|
|
65
|
+
description: Set reminders and scheduled tasks
|
|
66
|
+
---
|
|
67
|
+
When user wants to set a reminder:
|
|
68
|
+
|
|
69
|
+
1. Extract the message and time
|
|
70
|
+
2. Use `cron_create` tool with appropriate delay_minutes or cron_expression
|
|
71
|
+
3. Confirm the reminder was set
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
- "Remind me in 10 minutes" → delay_minutes=10
|
|
75
|
+
- "Remind me daily at 9am" → cron_expression="0 9 * * *"
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ensure_data_dirs():
|
|
80
|
+
"""Ensure all data directories exist."""
|
|
81
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
CODING_DIR.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def init_default_files():
|
|
88
|
+
"""Create default files if they don't exist."""
|
|
89
|
+
ensure_data_dirs()
|
|
90
|
+
|
|
91
|
+
# Create default CHARACTER.md
|
|
92
|
+
if not CHARACTER_FILE.exists():
|
|
93
|
+
CHARACTER_FILE.write_text(DEFAULT_CHARACTER, encoding="utf-8")
|
|
94
|
+
print(f" Created: {CHARACTER_FILE}")
|
|
95
|
+
|
|
96
|
+
# Create default skills
|
|
97
|
+
search_skill_dir = SKILLS_DIR / "search"
|
|
98
|
+
if not search_skill_dir.exists():
|
|
99
|
+
search_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
(search_skill_dir / "SKILL.md").write_text(
|
|
101
|
+
DEFAULT_SKILL_SEARCH, encoding="utf-8"
|
|
102
|
+
)
|
|
103
|
+
print(f" Created: {search_skill_dir / 'SKILL.md'}")
|
|
104
|
+
|
|
105
|
+
reminder_skill_dir = SKILLS_DIR / "reminder"
|
|
106
|
+
if not reminder_skill_dir.exists():
|
|
107
|
+
reminder_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
(reminder_skill_dir / "SKILL.md").write_text(
|
|
109
|
+
DEFAULT_SKILL_REMINDER, encoding="utf-8"
|
|
110
|
+
)
|
|
111
|
+
print(f" Created: {reminder_skill_dir / 'SKILL.md'}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def show_startup_info():
|
|
115
|
+
"""Display startup configuration info."""
|
|
116
|
+
print("\n" + "=" * 60)
|
|
117
|
+
print(" SquidBot Configuration")
|
|
118
|
+
print("=" * 60)
|
|
119
|
+
print(f" Home Directory : {DATA_DIR}")
|
|
120
|
+
print(f" Server Port : {SQUID_PORT}")
|
|
121
|
+
print(f" Model : {OPENAI_MODEL}")
|
|
122
|
+
print(f" Heartbeat : {HEARTBEAT_INTERVAL_MINUTES} minutes")
|
|
123
|
+
print("-" * 60)
|
|
124
|
+
print(f" Character File : {CHARACTER_FILE}")
|
|
125
|
+
print(f" Skills Dir : {SKILLS_DIR}")
|
|
126
|
+
print(f" Coding Dir : {CODING_DIR}")
|
|
127
|
+
print(f" Sessions Dir : {SESSIONS_DIR}")
|
|
128
|
+
print("=" * 60)
|
|
129
|
+
print("\n To customize, set environment variables:")
|
|
130
|
+
print(" SQUIDBOT_HOME=/path/to/home")
|
|
131
|
+
print(" SQUID_PORT=7777")
|
|
132
|
+
print(" OPENAI_MODEL=gpt-4o")
|
|
133
|
+
print("=" * 60 + "\n")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_config():
|
|
137
|
+
"""Validate required configuration."""
|
|
138
|
+
errors = []
|
|
139
|
+
if not TELEGRAM_BOT_TOKEN:
|
|
140
|
+
errors.append("TELEGRAM_BOT_TOKEN not set")
|
|
141
|
+
if not OPENAI_API_KEY:
|
|
142
|
+
errors.append("OPENAI_API_KEY not set")
|
|
143
|
+
if errors:
|
|
144
|
+
raise ValueError("Missing required config:\n" + "\n".join(errors))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Ensure data directory exists on import
|
|
148
|
+
ensure_data_dirs()
|