vedana-backoffice 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.
- vedana_backoffice/Caddyfile +17 -0
- vedana_backoffice/__init__.py +0 -0
- vedana_backoffice/components/__init__.py +0 -0
- vedana_backoffice/components/etl_graph.py +132 -0
- vedana_backoffice/components/ui_chat.py +236 -0
- vedana_backoffice/graph/__init__.py +0 -0
- vedana_backoffice/graph/build.py +169 -0
- vedana_backoffice/pages/__init__.py +0 -0
- vedana_backoffice/pages/chat.py +204 -0
- vedana_backoffice/pages/etl.py +353 -0
- vedana_backoffice/pages/eval.py +1006 -0
- vedana_backoffice/pages/jims_thread_list_page.py +894 -0
- vedana_backoffice/pages/main_dashboard.py +483 -0
- vedana_backoffice/py.typed +0 -0
- vedana_backoffice/start_services.py +39 -0
- vedana_backoffice/state.py +0 -0
- vedana_backoffice/states/__init__.py +0 -0
- vedana_backoffice/states/chat.py +368 -0
- vedana_backoffice/states/common.py +66 -0
- vedana_backoffice/states/etl.py +1590 -0
- vedana_backoffice/states/eval.py +1940 -0
- vedana_backoffice/states/jims.py +508 -0
- vedana_backoffice/states/main_dashboard.py +757 -0
- vedana_backoffice/ui.py +115 -0
- vedana_backoffice/util.py +71 -0
- vedana_backoffice/vedana_backoffice.py +23 -0
- vedana_backoffice-0.1.0.dist-info/METADATA +10 -0
- vedana_backoffice-0.1.0.dist-info/RECORD +30 -0
- vedana_backoffice-0.1.0.dist-info/WHEEL +4 -0
- vedana_backoffice-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, Iterable, Tuple
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
import orjson as json
|
|
10
|
+
import reflex as rx
|
|
11
|
+
import requests
|
|
12
|
+
from datapipe.compute import Catalog, run_pipeline
|
|
13
|
+
from jims_core.llms.llm_provider import env_settings as llm_settings
|
|
14
|
+
from jims_core.thread.thread_controller import ThreadController
|
|
15
|
+
from jims_core.util import uuid7
|
|
16
|
+
from vedana_core.settings import settings as core_settings
|
|
17
|
+
from vedana_etl.app import app as etl_app
|
|
18
|
+
from vedana_etl.pipeline import get_data_model_pipeline
|
|
19
|
+
|
|
20
|
+
from vedana_backoffice.states.common import MemLogger, get_vedana_app
|
|
21
|
+
from vedana_backoffice.states.jims import ThreadViewState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChatState(rx.State):
|
|
25
|
+
"""Minimal chatbot with per-answer technical details."""
|
|
26
|
+
|
|
27
|
+
input_text: str = ""
|
|
28
|
+
is_running: bool = False
|
|
29
|
+
messages: list[dict[str, Any]] = []
|
|
30
|
+
chat_thread_id: str = ""
|
|
31
|
+
data_model_text: str = ""
|
|
32
|
+
is_refreshing_dm: bool = False
|
|
33
|
+
provider: str = "openai" # default llm provider
|
|
34
|
+
model: str = core_settings.model
|
|
35
|
+
_default_models: tuple[str, ...] = (
|
|
36
|
+
"gpt-5.1-chat-latest",
|
|
37
|
+
"gpt-5.1",
|
|
38
|
+
"gpt-5-chat-latest",
|
|
39
|
+
"gpt-5",
|
|
40
|
+
"gpt-5-mini",
|
|
41
|
+
"gpt-5-nano",
|
|
42
|
+
"gpt-4.1",
|
|
43
|
+
"gpt-4.1-mini",
|
|
44
|
+
"gpt-4.1-nano",
|
|
45
|
+
"gpt-4o",
|
|
46
|
+
"gpt-4o-mini",
|
|
47
|
+
"o4-mini",
|
|
48
|
+
)
|
|
49
|
+
custom_openrouter_key: str = ""
|
|
50
|
+
default_openrouter_key_present: bool = bool(os.environ.get("OPENROUTER_API_KEY")) # to require api_key input
|
|
51
|
+
openai_models: list[str] = list(set(list(_default_models) + [core_settings.model]))
|
|
52
|
+
openrouter_models: list[str] = []
|
|
53
|
+
available_models: list[str] = list(set(list(_default_models) + [core_settings.model]))
|
|
54
|
+
enable_dm_filtering: bool = bool(os.environ.get("ENABLE_DM_FILTERING", False))
|
|
55
|
+
|
|
56
|
+
async def mount(self) -> None:
|
|
57
|
+
self.fetch_openrouter_models()
|
|
58
|
+
self._sync_available_models()
|
|
59
|
+
|
|
60
|
+
def set_input(self, value: str) -> None:
|
|
61
|
+
self.input_text = value
|
|
62
|
+
|
|
63
|
+
def set_model(self, value: str) -> None:
|
|
64
|
+
if value in self.available_models:
|
|
65
|
+
self.model = value
|
|
66
|
+
|
|
67
|
+
def set_custom_openrouter_key(self, value: str) -> None:
|
|
68
|
+
self.custom_openrouter_key = value # need validating?
|
|
69
|
+
|
|
70
|
+
def set_enable_dm_filtering(self, value: bool) -> None:
|
|
71
|
+
self.enable_dm_filtering = value
|
|
72
|
+
|
|
73
|
+
def set_provider(self, value: str) -> None:
|
|
74
|
+
self.provider = value
|
|
75
|
+
self._sync_available_models()
|
|
76
|
+
|
|
77
|
+
def _filter_chat_capable(self, models: Iterable[dict]) -> list[str]:
|
|
78
|
+
result: list[str] = []
|
|
79
|
+
for m in models:
|
|
80
|
+
model_id = str(m.get("id", "")).strip()
|
|
81
|
+
if not model_id:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
has_chat = False
|
|
85
|
+
architecture = m.get("architecture", {})
|
|
86
|
+
if architecture:
|
|
87
|
+
if "text" in architecture.get("input_modalities", []) and "text" in architecture.get(
|
|
88
|
+
"output_modalities", []
|
|
89
|
+
):
|
|
90
|
+
has_chat = True
|
|
91
|
+
|
|
92
|
+
has_tools = False # only accept models with tool calls
|
|
93
|
+
if "tools" in m.get("supported_parameters", []):
|
|
94
|
+
has_tools = True
|
|
95
|
+
|
|
96
|
+
if has_chat and has_tools:
|
|
97
|
+
result.append(model_id)
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
def fetch_openrouter_models(self) -> None:
|
|
102
|
+
try:
|
|
103
|
+
resp = requests.get(
|
|
104
|
+
f"{llm_settings.openrouter_api_base_url}/models",
|
|
105
|
+
# headers={"Authorization": f"Bearer {openrouter_api_key}"}, # actually works without a token as well
|
|
106
|
+
timeout=10,
|
|
107
|
+
)
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
payload = resp.json()
|
|
110
|
+
models = payload.get("data", [])
|
|
111
|
+
parsed = self._filter_chat_capable(models)
|
|
112
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
113
|
+
logging.warning(f"Failed to fetch OpenRouter models: {exc}")
|
|
114
|
+
parsed = []
|
|
115
|
+
self.openrouter_models = sorted(list(parsed))
|
|
116
|
+
|
|
117
|
+
def _sync_available_models(self) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Recompute available_models based on selected provider, and realign
|
|
120
|
+
the selected model if it is no longer valid.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
if self.provider == "openrouter":
|
|
124
|
+
models = self.openrouter_models
|
|
125
|
+
if not models:
|
|
126
|
+
self.provider = "openai"
|
|
127
|
+
models = self.openai_models
|
|
128
|
+
else:
|
|
129
|
+
models = self.openai_models
|
|
130
|
+
|
|
131
|
+
self.available_models = list(models)
|
|
132
|
+
|
|
133
|
+
if self.model not in self.available_models and self.available_models:
|
|
134
|
+
self.model = self.available_models[0]
|
|
135
|
+
|
|
136
|
+
def toggle_details_by_id(self, message_id: str) -> None:
|
|
137
|
+
for idx, m in enumerate(self.messages):
|
|
138
|
+
if str(m.get("id")) == str(message_id):
|
|
139
|
+
msg = dict(m)
|
|
140
|
+
msg["show_details"] = not bool(msg.get("show_details"))
|
|
141
|
+
self.messages[idx] = msg
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
def reset_session(self) -> None:
|
|
145
|
+
"""Clear current chat history and start a fresh session (new thread on next send)."""
|
|
146
|
+
self.messages = []
|
|
147
|
+
self.input_text = ""
|
|
148
|
+
self.chat_thread_id = ""
|
|
149
|
+
|
|
150
|
+
@rx.var
|
|
151
|
+
def total_conversation_cost(self) -> float:
|
|
152
|
+
"""Calculate total cost by summing requests_cost from all assistant messages."""
|
|
153
|
+
total = 0.0
|
|
154
|
+
for msg in self.messages:
|
|
155
|
+
if msg.get("is_assistant") and msg.get("model_stats"):
|
|
156
|
+
model_stats = msg.get("model_stats", {})
|
|
157
|
+
if isinstance(model_stats, dict):
|
|
158
|
+
# model_stats can be {model_name: {requests_cost: value, ...}}
|
|
159
|
+
for model_name, stats in model_stats.items():
|
|
160
|
+
if isinstance(stats, dict):
|
|
161
|
+
cost = stats.get("requests_cost")
|
|
162
|
+
if cost is not None:
|
|
163
|
+
try:
|
|
164
|
+
total += float(cost)
|
|
165
|
+
except (ValueError, TypeError):
|
|
166
|
+
pass
|
|
167
|
+
return total
|
|
168
|
+
|
|
169
|
+
@rx.var
|
|
170
|
+
def total_conversation_cost_str(self) -> str:
|
|
171
|
+
"""Formatted string representation of total conversation cost."""
|
|
172
|
+
cost = self.total_conversation_cost
|
|
173
|
+
if cost > 0:
|
|
174
|
+
return f"${cost:.6f}"
|
|
175
|
+
return ""
|
|
176
|
+
|
|
177
|
+
def _append_message(
|
|
178
|
+
self,
|
|
179
|
+
role: str,
|
|
180
|
+
content: str,
|
|
181
|
+
technical_info: dict[str, Any] | None = None,
|
|
182
|
+
debug_logs: str | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
message: dict[str, Any] = {
|
|
185
|
+
"id": str(uuid4()),
|
|
186
|
+
"role": role,
|
|
187
|
+
"content": content,
|
|
188
|
+
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
189
|
+
"is_assistant": role == "assistant",
|
|
190
|
+
"has_tech": False,
|
|
191
|
+
"show_details": False,
|
|
192
|
+
"tags": [],
|
|
193
|
+
"comments": [],
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if technical_info:
|
|
197
|
+
vts_list = [str(x) for x in technical_info.get("vts_queries") or []]
|
|
198
|
+
cypher_list = [str(x) for x in technical_info.get("cypher_queries") or []]
|
|
199
|
+
models_raw = technical_info.get("model_stats", {})
|
|
200
|
+
model_pairs: list[tuple[str, str]] = []
|
|
201
|
+
if isinstance(models_raw, dict):
|
|
202
|
+
for mk, mv in models_raw.items():
|
|
203
|
+
try:
|
|
204
|
+
model_pairs.append((str(mk), json.dumps(mv).decode()))
|
|
205
|
+
except Exception:
|
|
206
|
+
model_pairs.append((str(mk), str(mv)))
|
|
207
|
+
models_list = [f"{k}: {v}" for k, v in model_pairs]
|
|
208
|
+
|
|
209
|
+
message["vts_str"] = "\n".join(vts_list)
|
|
210
|
+
message["cypher_str"] = "\n".join(cypher_list)
|
|
211
|
+
message["models_str"] = "\n".join(models_list)
|
|
212
|
+
message["has_vts"] = bool(vts_list)
|
|
213
|
+
message["has_cypher"] = bool(cypher_list)
|
|
214
|
+
message["has_models"] = bool(models_list)
|
|
215
|
+
message["has_tech"] = bool(vts_list or cypher_list or models_list)
|
|
216
|
+
message["model_stats"] = models_raw
|
|
217
|
+
|
|
218
|
+
logs = (debug_logs or "").replace("\r\n", "\n").rstrip()
|
|
219
|
+
message["logs_str"] = logs
|
|
220
|
+
message["has_logs"] = bool(logs)
|
|
221
|
+
|
|
222
|
+
self.messages.append(message)
|
|
223
|
+
|
|
224
|
+
async def _ensure_thread(self) -> str:
|
|
225
|
+
vedana_app = await get_vedana_app()
|
|
226
|
+
try:
|
|
227
|
+
existing = await ThreadController.from_thread_id(vedana_app.sessionmaker, UUID(self.chat_thread_id))
|
|
228
|
+
except Exception:
|
|
229
|
+
existing = None
|
|
230
|
+
|
|
231
|
+
if existing is None:
|
|
232
|
+
thread_id = uuid7()
|
|
233
|
+
ctl = await ThreadController.new_thread(
|
|
234
|
+
vedana_app.sessionmaker,
|
|
235
|
+
contact_id=f"reflex:{thread_id}",
|
|
236
|
+
thread_id=thread_id,
|
|
237
|
+
thread_config={"interface": "reflex"},
|
|
238
|
+
)
|
|
239
|
+
return str(ctl.thread.thread_id)
|
|
240
|
+
|
|
241
|
+
return self.chat_thread_id
|
|
242
|
+
|
|
243
|
+
async def _run_message(self, thread_id: str, user_text: str) -> Tuple[str, Dict[str, Any], str]:
|
|
244
|
+
vedana_app = await get_vedana_app()
|
|
245
|
+
try:
|
|
246
|
+
tid = UUID(thread_id)
|
|
247
|
+
except Exception:
|
|
248
|
+
tid = uuid7()
|
|
249
|
+
|
|
250
|
+
ctl = await ThreadController.from_thread_id(vedana_app.sessionmaker, tid)
|
|
251
|
+
if ctl is None:
|
|
252
|
+
thr_id = uuid7()
|
|
253
|
+
ctl = await ThreadController.new_thread(
|
|
254
|
+
vedana_app.sessionmaker,
|
|
255
|
+
contact_id=f"reflex:{thr_id}",
|
|
256
|
+
thread_id=thr_id,
|
|
257
|
+
thread_config={"interface": "reflex"},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
mem_logger = MemLogger("rag_debug", level=logging.DEBUG)
|
|
261
|
+
|
|
262
|
+
await ctl.store_user_message(uuid7(), user_text)
|
|
263
|
+
|
|
264
|
+
pipeline = vedana_app.pipeline
|
|
265
|
+
pipeline.logger = mem_logger
|
|
266
|
+
pipeline.model = f"{self.provider}/{self.model}"
|
|
267
|
+
pipeline.enable_filtering = self.enable_dm_filtering
|
|
268
|
+
|
|
269
|
+
ctx = await ctl.make_context()
|
|
270
|
+
|
|
271
|
+
# override model api_key if custom api_key is provided
|
|
272
|
+
if self.custom_openrouter_key and self.provider == "openrouter":
|
|
273
|
+
ctx.llm.model_api_key = self.custom_openrouter_key
|
|
274
|
+
# embeddings model is not customisable in chat, it's configured on project level.
|
|
275
|
+
|
|
276
|
+
events = await ctl.run_pipeline_with_context(pipeline, ctx)
|
|
277
|
+
|
|
278
|
+
answer: str = ""
|
|
279
|
+
tech: dict[str, Any] = {}
|
|
280
|
+
for ev in events:
|
|
281
|
+
if ev.event_type == "comm.assistant_message":
|
|
282
|
+
answer = str(ev.event_data.get("content", ""))
|
|
283
|
+
elif ev.event_type == "rag.query_processed":
|
|
284
|
+
tech = dict(ev.event_data.get("technical_info", {}))
|
|
285
|
+
|
|
286
|
+
logs = mem_logger.get_logs()
|
|
287
|
+
return answer, tech, logs
|
|
288
|
+
|
|
289
|
+
# mypy raises error: "EventNamespace" not callable [operator], though event definition is according to reflex docs
|
|
290
|
+
@rx.event(background=True) # type: ignore[operator]
|
|
291
|
+
async def send_background(self, user_text: str):
|
|
292
|
+
"""This runs in background (non-blocking), after send() submission."""
|
|
293
|
+
# Need context to modify state safely
|
|
294
|
+
async with self:
|
|
295
|
+
try:
|
|
296
|
+
thread_id = await self._ensure_thread()
|
|
297
|
+
answer, tech, logs = await self._run_message(thread_id, user_text)
|
|
298
|
+
# update shared session thread id
|
|
299
|
+
self.chat_thread_id = thread_id
|
|
300
|
+
except Exception as e:
|
|
301
|
+
answer, tech, logs = (f"Error: {e}", {}, traceback.format_exc())
|
|
302
|
+
|
|
303
|
+
self._append_message("assistant", answer, technical_info=tech, debug_logs=logs)
|
|
304
|
+
self.is_running = False
|
|
305
|
+
|
|
306
|
+
def send(self):
|
|
307
|
+
"""form submit / button click"""
|
|
308
|
+
if self.is_running:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
user_text = (self.input_text or "").strip()
|
|
312
|
+
if not user_text:
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
self._append_message("user", user_text)
|
|
316
|
+
self.input_text = ""
|
|
317
|
+
self.is_running = True
|
|
318
|
+
yield # trigger UI update
|
|
319
|
+
|
|
320
|
+
yield ChatState.send_background(user_text)
|
|
321
|
+
|
|
322
|
+
def open_jims_thread(self):
|
|
323
|
+
"""Open the JIMS page and preselect the current chat thread."""
|
|
324
|
+
if not (self.chat_thread_id or "").strip():
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# First set selection on JIMS state, then navigate to /jims
|
|
328
|
+
yield ThreadViewState.select_thread(thread_id=self.chat_thread_id) # type: ignore[operator]
|
|
329
|
+
yield rx.redirect("/jims") # type: ignore[operator]
|
|
330
|
+
|
|
331
|
+
@rx.event(background=True) # type: ignore[operator]
|
|
332
|
+
async def load_data_model_text(self):
|
|
333
|
+
async with self:
|
|
334
|
+
va = await get_vedana_app()
|
|
335
|
+
try:
|
|
336
|
+
self.data_model_text = await va.data_model.to_text_descr()
|
|
337
|
+
except Exception:
|
|
338
|
+
self.data_model_text = "(failed to load data model text)"
|
|
339
|
+
|
|
340
|
+
self.is_refreshing_dm = False # make the update button available
|
|
341
|
+
yield
|
|
342
|
+
|
|
343
|
+
def reload_data_model(self):
|
|
344
|
+
"""Connecting button with a background task. Used to trigger animations properly."""
|
|
345
|
+
if self.is_refreshing_dm:
|
|
346
|
+
return
|
|
347
|
+
self.is_refreshing_dm = True
|
|
348
|
+
yield
|
|
349
|
+
yield ChatState.reload_data_model_background()
|
|
350
|
+
|
|
351
|
+
@rx.event(background=True) # type: ignore[operator]
|
|
352
|
+
async def reload_data_model_background(self):
|
|
353
|
+
try:
|
|
354
|
+
"""Reload the data model by running all data_model_steps from the pipeline."""
|
|
355
|
+
await asyncio.to_thread(run_pipeline, etl_app.ds, Catalog({}), get_data_model_pipeline())
|
|
356
|
+
async with self:
|
|
357
|
+
va = await get_vedana_app()
|
|
358
|
+
self.data_model_text = await va.data_model.to_text_descr()
|
|
359
|
+
yield rx.toast.success("Data model reloaded")
|
|
360
|
+
except Exception as e:
|
|
361
|
+
async with self:
|
|
362
|
+
error_msg = str(e)
|
|
363
|
+
self.data_model_text = f"(error reloading data model: {error_msg})"
|
|
364
|
+
yield rx.toast.error(f"Failed to reload data model\n{error_msg}")
|
|
365
|
+
finally:
|
|
366
|
+
async with self:
|
|
367
|
+
self.is_refreshing_dm = False
|
|
368
|
+
yield
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import reflex as rx
|
|
6
|
+
import requests
|
|
7
|
+
from vedana_core.app import VedanaApp, make_vedana_app
|
|
8
|
+
|
|
9
|
+
vedana_app: VedanaApp | None = None
|
|
10
|
+
|
|
11
|
+
EVAL_ENABLED = bool(os.environ.get("GRIST_TEST_SET_DOC_ID"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def get_vedana_app():
|
|
15
|
+
global vedana_app
|
|
16
|
+
if vedana_app is None:
|
|
17
|
+
vedana_app = await make_vedana_app()
|
|
18
|
+
return vedana_app
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MemLogger(logging.Logger):
|
|
22
|
+
"""Logger that captures logs to a string buffer for debugging purposes."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: str, level: int = 0) -> None:
|
|
25
|
+
super().__init__(name, level)
|
|
26
|
+
self.parent = logging.getLogger(__name__)
|
|
27
|
+
self._buf = io.StringIO()
|
|
28
|
+
handler = logging.StreamHandler(self._buf)
|
|
29
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
|
30
|
+
self.addHandler(handler)
|
|
31
|
+
|
|
32
|
+
def get_logs(self) -> str:
|
|
33
|
+
return self._buf.getvalue()
|
|
34
|
+
|
|
35
|
+
def clear(self) -> None:
|
|
36
|
+
self._buf.truncate(0)
|
|
37
|
+
self._buf.seek(0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AppVersionState(rx.State):
|
|
41
|
+
version: str = f"`{os.environ.get('VERSION', 'unspecified_version')}`" # md-formatted
|
|
42
|
+
eval_enabled: bool = EVAL_ENABLED
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TelegramBotState(rx.State):
|
|
46
|
+
"""State for Telegram bot information."""
|
|
47
|
+
|
|
48
|
+
bot_username: str = ""
|
|
49
|
+
bot_url: str = ""
|
|
50
|
+
has_bot: bool = False
|
|
51
|
+
|
|
52
|
+
def load_bot_info(self) -> None:
|
|
53
|
+
token = os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
54
|
+
if not token:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
bot_status = requests.get(f"https://api.telegram.org/bot{token}/getMe")
|
|
59
|
+
if bot_status.status_code == 200:
|
|
60
|
+
bot_status = bot_status.json()
|
|
61
|
+
if bot_status["ok"]:
|
|
62
|
+
self.bot_username = bot_status["result"]["username"]
|
|
63
|
+
self.bot_url = f"https://t.me/{self.bot_username}"
|
|
64
|
+
self.has_bot = True
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logging.warning(f"Failed to load Telegram bot info: {e}")
|