notebookvb
This commit is contained in:
@@ -2,14 +2,18 @@
|
|||||||
# Připojení k Firebird databázi Medicus (medicus.fdb). Volí DSN podle názvu počítače.
|
# Připojení k Firebird databázi Medicus (medicus.fdb). Volí DSN podle názvu počítače.
|
||||||
# Obsahuje třídu MedicusDB s metodami pro dotazy na pacienty, registrace a faktury.
|
# Obsahuje třídu MedicusDB s metodami pro dotazy na pacienty, registrace a faktury.
|
||||||
|
|
||||||
|
import os
|
||||||
import socket
|
import socket
|
||||||
import fdb
|
import fdb
|
||||||
|
|
||||||
|
|
||||||
def get_medicus_connection():
|
def get_medicus_connection():
|
||||||
"""
|
"""
|
||||||
Připojí se k Firebird medicus.fdb podle názvu počítače.
|
Připojí se k Firebird medicus.fdb. DSN se vybere takto:
|
||||||
Vrátí fdb.Connection nebo vyhodí RuntimeError pro neznámý počítač.
|
1) env MEDICUS_FDB_DSN (má přednost — nutné v dockeru, kde hostname = ID kontejneru),
|
||||||
|
2) podle názvu počítače (dsn_map),
|
||||||
|
3) default.
|
||||||
|
Vrátí fdb.Connection nebo vyhodí RuntimeError.
|
||||||
"""
|
"""
|
||||||
computer_name = socket.gethostname().upper()
|
computer_name = socket.gethostname().upper()
|
||||||
dsn_map = {
|
dsn_map = {
|
||||||
@@ -20,7 +24,8 @@ def get_medicus_connection():
|
|||||||
"Z230": r"192.168.1.76:/firebird/data/medicus.fdb", # přepnuto z reporteru na tower 2026-06-14
|
"Z230": r"192.168.1.76:/firebird/data/medicus.fdb", # přepnuto z reporteru na tower 2026-06-14
|
||||||
"TOWER": r"192.168.1.76:/firebird/data/medicus.fdb", # Firebird 2.5 docker kontejner na toweru
|
"TOWER": r"192.168.1.76:/firebird/data/medicus.fdb", # Firebird 2.5 docker kontejner na toweru
|
||||||
}
|
}
|
||||||
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
|
dsn = (os.environ.get("MEDICUS_FDB_DSN")
|
||||||
|
or dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb"))
|
||||||
import sys
|
import sys
|
||||||
print(f"[medicus_db] Pripojuji se jako {computer_name} -> {dsn}", file=sys.stderr, flush=True)
|
print(f"[medicus_db] Pripojuji se jako {computer_name} -> {dsn}", file=sys.stderr, flush=True)
|
||||||
return fdb.connect(dsn=dsn, user="SYSDBA", password="masterkey", charset="win1250")
|
return fdb.connect(dsn=dsn, user="SYSDBA", password="masterkey", charset="win1250")
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""
|
||||||
|
telegram_notify.py
|
||||||
|
------------------
|
||||||
|
Notifikace a obousměrná komunikace přes Telegram Bot API
|
||||||
|
(bot ClaudeBot @Vlado_Claude_Bot).
|
||||||
|
|
||||||
|
Token a výchozí chat_id se načítají z `Medevio/.env`:
|
||||||
|
TELEGRAM_BOT_TOKEN=123456789:AAE...
|
||||||
|
TELEGRAM_CHAT_ID=6639316354
|
||||||
|
|
||||||
|
Použití ze skriptu:
|
||||||
|
from Knihovny.telegram_notify import posli_telegram, zeptej_se_telegram
|
||||||
|
|
||||||
|
posli_telegram("Pipeline 08 hotová, 142 záznamů")
|
||||||
|
|
||||||
|
odpoved = zeptej_se_telegram("Mám reimportovat i archiv? (ano/ne)")
|
||||||
|
if odpoved and odpoved.strip().lower() == "ano":
|
||||||
|
...
|
||||||
|
|
||||||
|
Použití z příkazové řádky:
|
||||||
|
python -m Knihovny.telegram_notify "Hotovo"
|
||||||
|
python -m Knihovny.telegram_notify --ask "Pokracovat? (ano/ne)"
|
||||||
|
|
||||||
|
POZN.: getUpdates smí v jednu chvíli pollovat jen JEDEN proces. Pokud běží
|
||||||
|
víc skriptů naráz, které čekají na odpověď, kradou si navzájem zprávy —
|
||||||
|
v praxi se ptá vždy jen jeden agent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Načtení .env (Medevio/.env)
|
||||||
|
# =========================
|
||||||
|
def _load_env():
|
||||||
|
env_path = Path(__file__).resolve().parent.parent / "Medevio" / ".env"
|
||||||
|
if env_path.exists():
|
||||||
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if "=" in line and not line.startswith("#"):
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
os.environ.setdefault(k.strip(), v.strip())
|
||||||
|
|
||||||
|
|
||||||
|
_load_env()
|
||||||
|
|
||||||
|
|
||||||
|
API_BASE = "https://api.telegram.org/bot{token}/{method}"
|
||||||
|
|
||||||
|
|
||||||
|
def _token() -> str:
|
||||||
|
token = os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||||
|
if not token:
|
||||||
|
raise RuntimeError("Chybí TELEGRAM_BOT_TOKEN v Medevio/.env")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_chat_id(chat_id: str | None) -> str:
|
||||||
|
chat_id = chat_id or os.environ.get("TELEGRAM_CHAT_ID")
|
||||||
|
if not chat_id:
|
||||||
|
raise RuntimeError("Chybí TELEGRAM_CHAT_ID (zadej argumentem nebo v Medevio/.env)")
|
||||||
|
return str(chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _call(method: str, *, http_timeout: int = 15, **params):
|
||||||
|
"""Zavolá Telegram Bot API metodu a vrátí pole `result`."""
|
||||||
|
url = API_BASE.format(token=_token(), method=method)
|
||||||
|
r = requests.post(url, json=params, timeout=http_timeout)
|
||||||
|
data = r.json()
|
||||||
|
if not data.get("ok"):
|
||||||
|
raise RuntimeError(f"Telegram {method} selhal [{r.status_code}]: {data}")
|
||||||
|
return data["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def posli_telegram(
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
chat_id: str | None = None,
|
||||||
|
parse_mode: str | None = None,
|
||||||
|
disable_notification: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Pošle zprávu přes Telegram bota.
|
||||||
|
|
||||||
|
:param text: text zprávy (max 4096 znaků)
|
||||||
|
:param chat_id: cílový chat; výchozí z TELEGRAM_CHAT_ID
|
||||||
|
:param parse_mode: None | "Markdown" | "MarkdownV2" | "HTML"
|
||||||
|
:param disable_notification: True = tichá zpráva (bez upozornění)
|
||||||
|
:return: odeslaná zpráva (dict z Telegram API)
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"chat_id": _resolve_chat_id(chat_id),
|
||||||
|
"text": text,
|
||||||
|
"disable_notification": disable_notification,
|
||||||
|
}
|
||||||
|
if parse_mode:
|
||||||
|
params["parse_mode"] = parse_mode
|
||||||
|
return _call("sendMessage", **params)
|
||||||
|
|
||||||
|
|
||||||
|
def zeptej_se_telegram(
|
||||||
|
otazka: str,
|
||||||
|
*,
|
||||||
|
chat_id: str | None = None,
|
||||||
|
timeout: int = 300,
|
||||||
|
poll_timeout: int = 30,
|
||||||
|
parse_mode: str | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Pošle otázku a BLOKUJÍCÍ čeká na textovou odpověď uživatele.
|
||||||
|
|
||||||
|
Zahodí starší zprávy a bere jen tu, která přijde PO odeslání otázky.
|
||||||
|
|
||||||
|
:param otazka: text otázky
|
||||||
|
:param chat_id: cílový chat; výchozí z TELEGRAM_CHAT_ID
|
||||||
|
:param timeout: celkové čekání na odpověď v sekundách (pak vrátí None)
|
||||||
|
:param poll_timeout: délka jednoho long-poll cyklu v sekundách
|
||||||
|
:param parse_mode: formátování otázky (None | "HTML" | "Markdown")
|
||||||
|
:return: text odpovědi, nebo None když nikdo neodpoví do timeoutu
|
||||||
|
"""
|
||||||
|
cid = _resolve_chat_id(chat_id)
|
||||||
|
|
||||||
|
# Zjisti poslední update_id, ať bereme jen NOVÉ zprávy po otázce.
|
||||||
|
existujici = _call("getUpdates", http_timeout=15)
|
||||||
|
offset = (existujici[-1]["update_id"] + 1) if existujici else 0
|
||||||
|
|
||||||
|
posli_telegram(otazka, chat_id=cid, parse_mode=parse_mode)
|
||||||
|
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
zbyva = int(deadline - time.monotonic())
|
||||||
|
if zbyva <= 0:
|
||||||
|
break
|
||||||
|
lp = max(1, min(poll_timeout, zbyva))
|
||||||
|
updates = _call("getUpdates", http_timeout=lp + 10, offset=offset, timeout=lp)
|
||||||
|
for u in updates:
|
||||||
|
offset = u["update_id"] + 1
|
||||||
|
msg = u.get("message") or {}
|
||||||
|
if str(msg.get("chat", {}).get("id")) != cid:
|
||||||
|
continue # zpráva z jiného chatu — ignoruj
|
||||||
|
text = msg.get("text")
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_print(text: str):
|
||||||
|
"""Výpis odolný vůči kódování Windows konzole (cp1252)."""
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
print(text.encode("ascii", "replace").decode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Ať projdou i diakritika/emoji na Windows konzoli.
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
print('Použití:')
|
||||||
|
print(' python -m Knihovny.telegram_notify "text zprávy"')
|
||||||
|
print(' python -m Knihovny.telegram_notify --ask "otázka?"')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args[0] == "--ask":
|
||||||
|
otazka = " ".join(args[1:]) or "?"
|
||||||
|
odpoved = zeptej_se_telegram(otazka, timeout=240)
|
||||||
|
if odpoved is None:
|
||||||
|
_safe_print("(bez odpovědi — vypršel timeout)")
|
||||||
|
sys.exit(2)
|
||||||
|
_safe_print(odpoved)
|
||||||
|
else:
|
||||||
|
posli_telegram(" ".join(args))
|
||||||
|
_safe_print("Odesláno OK")
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
"""
|
||||||
|
telegram_user.py
|
||||||
|
----------------
|
||||||
|
Ovládání PLNOHODNOTNÉHO Telegram účtu (ne bota) přes user API (MTProto / Telethon).
|
||||||
|
Na rozdíl od bota umí napsat komukoli a unese VÍCE souběžných agentů na jednom účtu
|
||||||
|
(jako Telegram otevřený zároveň na PC, tabletu i mobilu).
|
||||||
|
|
||||||
|
⚠️ Jedná JMÉNEM přihlášeného účtu. Session soubor = plný přístup k účtu.
|
||||||
|
⚠️ Nové účty na automatizaci Telegram rychle banuje (zvlášť VoIP čísla — použij reálnou SIM).
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
VÍCE AGENTŮ NA JEDNOM ÚČTU
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
- api_id/api_hash se SDÍLÍ (identifikují „aplikaci", ne zařízení).
|
||||||
|
- Každý agent musí mít VLASTNÍ session soubor (= vlastní autorizace / „zařízení").
|
||||||
|
Sdílet jednu session mezi procesy NELZE (database is locked / AUTH_KEY_DUPLICATED).
|
||||||
|
→ každý agent se přihlásí zvlášť: `login --jako <jmeno>` (jeden SMS kód na agenta).
|
||||||
|
- Všechny sessions vidí stejný chat, proto se odpovědi směrují přes Telegram **Reply**:
|
||||||
|
agent pošle označenou otázku a bere jen tu odpověď, která je Reply na *jeho* zprávu
|
||||||
|
(shoda `reply_to_msg_id`). Tím se odpovědi více agentů nepomíchají.
|
||||||
|
|
||||||
|
Konfigurace v `Medevio/.env` (api_id/api_hash z https://my.telegram.org):
|
||||||
|
TELEGRAM_API_ID=1234567
|
||||||
|
TELEGRAM_API_HASH=abcdef0123456789abcdef0123456789
|
||||||
|
TELEGRAM_PHONE=+420... # nepovinné (jinak se zeptá při loginu)
|
||||||
|
|
||||||
|
Session soubory: `Medevio/agent_telegram_<jmeno>.session` (gitignored).
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
CLI
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
Jednorázové přihlášení agenta (spusť ve svém terminálu — čeká na kód z SMS):
|
||||||
|
python -m Knihovny.telegram_user login --jako recepty
|
||||||
|
python -m Knihovny.telegram_user login --jako kalendar
|
||||||
|
|
||||||
|
Poslání zprávy ("me" = Uložené zprávy / Saved Messages):
|
||||||
|
python -m Knihovny.telegram_user send me "Test" --jako recepty
|
||||||
|
|
||||||
|
Otázka + čekání na Reply odpověď (vypíše odpověď na stdout):
|
||||||
|
python -m Knihovny.telegram_user ask recepty "Mam pokracovat? (ano/ne)"
|
||||||
|
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
ZE SKRIPTU
|
||||||
|
────────────────────────────────────────────────────────────────────────
|
||||||
|
from Knihovny.telegram_user import posli_jako_ja, zeptej_se_jako
|
||||||
|
|
||||||
|
posli_jako_ja("me", "Pipeline 08 hotová", session="recepty")
|
||||||
|
|
||||||
|
odp = zeptej_se_jako("recepty", "Našel jsem 3 sporné záznamy. Pokračovat?")
|
||||||
|
if odp and odp.strip().lower() == "ano":
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# telethon.sync zpřístupní metody synchronně (bez async/await)
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
from telethon.errors import SessionPasswordNeededError, PhoneNumberUnoccupiedError
|
||||||
|
|
||||||
|
|
||||||
|
def _load_env():
|
||||||
|
env_path = Path(__file__).resolve().parent.parent / "Medevio" / ".env"
|
||||||
|
if env_path.exists():
|
||||||
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if "=" in line and not line.startswith("#"):
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
os.environ.setdefault(k.strip(), v.strip())
|
||||||
|
|
||||||
|
|
||||||
|
_load_env()
|
||||||
|
|
||||||
|
|
||||||
|
def _api_id() -> int:
|
||||||
|
val = os.environ.get("TELEGRAM_API_ID")
|
||||||
|
if not val:
|
||||||
|
raise RuntimeError("Chybí TELEGRAM_API_ID v Medevio/.env (z https://my.telegram.org)")
|
||||||
|
return int(val)
|
||||||
|
|
||||||
|
|
||||||
|
def _api_hash() -> str:
|
||||||
|
val = os.environ.get("TELEGRAM_API_HASH")
|
||||||
|
if not val:
|
||||||
|
raise RuntimeError("Chybí TELEGRAM_API_HASH v Medevio/.env (z https://my.telegram.org)")
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def _session_path(jmeno: str | None) -> Path:
|
||||||
|
base = f"agent_telegram_{jmeno}" if jmeno else "agent_telegram"
|
||||||
|
return Path(__file__).resolve().parent.parent / "Medevio" / base
|
||||||
|
|
||||||
|
|
||||||
|
def _new_client(session: str | None = None) -> TelegramClient:
|
||||||
|
return TelegramClient(str(_session_path(session)), _api_id(), _api_hash())
|
||||||
|
|
||||||
|
|
||||||
|
def prihlas(session: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Jednorázové přihlášení dané session. Interaktivně se zeptá na kód z SMS
|
||||||
|
a případně na heslo dvoufázového ověření. Vytvoří session soubor.
|
||||||
|
SPOUŠTĚJ V TERMINÁLU (potřebuje input).
|
||||||
|
"""
|
||||||
|
client = _new_client(session)
|
||||||
|
client.start(phone=os.environ.get("TELEGRAM_PHONE") or (lambda: input("Telefon (+420...): ")))
|
||||||
|
me = client.get_me()
|
||||||
|
print(f"Session '{session or 'default'}' přihlášena jako "
|
||||||
|
f"{me.first_name or ''} (@{me.username}) id={me.id}")
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def _phone() -> str:
|
||||||
|
val = os.environ.get("TELEGRAM_PHONE")
|
||||||
|
if not val:
|
||||||
|
raise RuntimeError("Chybí TELEGRAM_PHONE v Medevio/.env")
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def login_posli_kod(session: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
1. krok přihlášení (řízeného na dálku): vyžádá si od Telegramu kód.
|
||||||
|
Vytiskne `PHONE_CODE_HASH=...`, který je potřeba pro 2. krok.
|
||||||
|
"""
|
||||||
|
client = _new_client(session)
|
||||||
|
client.connect()
|
||||||
|
try:
|
||||||
|
sent = client.send_code_request(_phone())
|
||||||
|
print("PHONE_CODE_HASH=" + sent.phone_code_hash)
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def login_dokonci(code, phone_code_hash: str, session: str | None = None,
|
||||||
|
password: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
2. krok přihlášení: dokončí login zadaným kódem (a případně heslem 2FA).
|
||||||
|
Při úspěchu uloží session soubor.
|
||||||
|
"""
|
||||||
|
client = _new_client(session)
|
||||||
|
client.connect()
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
client.sign_in(phone=_phone(), code=str(code), phone_code_hash=phone_code_hash)
|
||||||
|
except SessionPasswordNeededError:
|
||||||
|
if not password:
|
||||||
|
print("NEED_PASSWORD")
|
||||||
|
return
|
||||||
|
client.sign_in(password=password)
|
||||||
|
except PhoneNumberUnoccupiedError:
|
||||||
|
print("UCET_NEEXISTUJE - nejdriv zaregistruj cislo v aplikaci Telegram")
|
||||||
|
return
|
||||||
|
me = client.get_me()
|
||||||
|
print(f"OK prihlaseno jako {me.first_name or ''} (@{me.username}) id={me.id}")
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def posli_jako_ja(komu, text: str, *, session: str | None = None):
|
||||||
|
"""
|
||||||
|
Pošle zprávu jménem přihlášeného účtu z dané session.
|
||||||
|
|
||||||
|
:param komu: "me" (Saved Messages) | "@username" | telefon | int id
|
||||||
|
:param text: text zprávy
|
||||||
|
:param session: jméno session (které přihlášení použít)
|
||||||
|
:return: odeslaná zpráva (Telethon Message)
|
||||||
|
"""
|
||||||
|
with _new_client(session) as client:
|
||||||
|
if not client.is_user_authorized():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Session '{session or 'default'}' není přihlášena — "
|
||||||
|
f"spusť: python -m Knihovny.telegram_user login"
|
||||||
|
+ (f" --jako {session}" if session else "")
|
||||||
|
)
|
||||||
|
return client.send_message(komu, text)
|
||||||
|
|
||||||
|
|
||||||
|
def precti_zpravy(komu, limit: int = 10, *, session: str | None = None):
|
||||||
|
"""
|
||||||
|
Vrátí posledních `limit` zpráv z daného chatu.
|
||||||
|
|
||||||
|
:return: list dictů {"id", "text", "odeslal_ja", "reply_na", "datum"}
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
with _new_client(session) as client:
|
||||||
|
if not client.is_user_authorized():
|
||||||
|
raise RuntimeError(f"Session '{session or 'default'}' není přihlášena.")
|
||||||
|
for msg in client.iter_messages(komu, limit=limit):
|
||||||
|
out.append({
|
||||||
|
"id": msg.id,
|
||||||
|
"text": msg.text or "",
|
||||||
|
"odeslal_ja": bool(msg.out),
|
||||||
|
"reply_na": msg.reply_to_msg_id,
|
||||||
|
"datum": msg.date,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def zeptej_se_jako(
|
||||||
|
agent: str,
|
||||||
|
otazka: str,
|
||||||
|
*,
|
||||||
|
komu="me",
|
||||||
|
session: str | None = None,
|
||||||
|
timeout: int = 300,
|
||||||
|
poll_interval: int = 3,
|
||||||
|
vyzaduj_reply: bool = True,
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
Pošle označenou otázku ("[agent] otázka") a BLOKUJÍCÍ čeká na odpověď.
|
||||||
|
|
||||||
|
Při více agentech naráz se odpovědi rozlišují přes Telegram **Reply**:
|
||||||
|
bere jen tu příchozí zprávu, která je Reply na právě odeslanou otázku.
|
||||||
|
|
||||||
|
:param agent: jméno agenta (objeví se v textu otázky jako štítek)
|
||||||
|
:param otazka: text otázky
|
||||||
|
:param komu: kam poslat ("me" = Saved Messages | "@username" | id)
|
||||||
|
:param session: jméno session; výchozí = `agent` (každý agent svůj soubor)
|
||||||
|
:param timeout: celkové čekání v sekundách (pak vrátí None)
|
||||||
|
:param poll_interval: jak často kontrolovat nové zprávy (s)
|
||||||
|
:param vyzaduj_reply: True = bere jen Reply na svou otázku (bezpečné pro víc agentů);
|
||||||
|
False = vezme první příchozí zprávu (jen pro 1 agenta)
|
||||||
|
:return: text odpovědi, nebo None při timeoutu
|
||||||
|
"""
|
||||||
|
session = session or agent
|
||||||
|
with _new_client(session) as client:
|
||||||
|
if not client.is_user_authorized():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Session '{session}' není přihlášena — "
|
||||||
|
f"spusť: python -m Knihovny.telegram_user login --jako {session}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sent = client.send_message(komu, f"[{agent}] {otazka}")
|
||||||
|
qid = sent.id
|
||||||
|
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
# jen zprávy novější než naše otázka, od nejstarší
|
||||||
|
for msg in client.iter_messages(komu, min_id=qid, reverse=True):
|
||||||
|
if msg.out:
|
||||||
|
continue # naše vlastní zpráva
|
||||||
|
if vyzaduj_reply:
|
||||||
|
if msg.reply_to_msg_id == qid:
|
||||||
|
return msg.text
|
||||||
|
else:
|
||||||
|
return msg.text
|
||||||
|
zbyva = deadline - time.monotonic()
|
||||||
|
if zbyva <= 0:
|
||||||
|
break
|
||||||
|
time.sleep(min(poll_interval, max(1, zbyva)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_print(text: str):
|
||||||
|
try:
|
||||||
|
print(text)
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
print(text.encode("ascii", "replace").decode("ascii"))
|
||||||
|
|
||||||
|
|
||||||
|
def _main():
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(prog="telegram_user", description="Telegram user účet (Telethon)")
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p_login = sub.add_parser("login", help="jednorázové přihlášení session")
|
||||||
|
p_login.add_argument("--jako", dest="jako", default=None, help="jméno session/agenta")
|
||||||
|
|
||||||
|
p_send = sub.add_parser("send", help="poslat zprávu")
|
||||||
|
p_send.add_argument("komu", help='"me" | "@username" | telefon | id')
|
||||||
|
p_send.add_argument("text", help="text zprávy")
|
||||||
|
p_send.add_argument("--jako", dest="jako", default=None, help="jméno session")
|
||||||
|
|
||||||
|
p_ask = sub.add_parser("ask", help="poslat otázku a počkat na Reply odpověď")
|
||||||
|
p_ask.add_argument("agent", help="jméno agenta (štítek + výchozí session)")
|
||||||
|
p_ask.add_argument("text", help="text otázky")
|
||||||
|
p_ask.add_argument("--komu", dest="komu", default="me", help='kam (výchozí "me")')
|
||||||
|
p_ask.add_argument("--timeout", dest="timeout", type=int, default=240, help="čekání v s")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.cmd == "login":
|
||||||
|
prihlas(args.jako)
|
||||||
|
elif args.cmd == "send":
|
||||||
|
posli_jako_ja(args.komu, args.text, session=args.jako)
|
||||||
|
_safe_print("Odesláno OK")
|
||||||
|
elif args.cmd == "ask":
|
||||||
|
odp = zeptej_se_jako(args.agent, args.text, komu=args.komu, timeout=args.timeout)
|
||||||
|
if odp is None:
|
||||||
|
_safe_print("(bez odpovědi — vypršel timeout)")
|
||||||
|
sys.exit(2)
|
||||||
|
_safe_print(odp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_main()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# MedicusFirebird — Firebird 2.5 zrcadlo Medicus DB na toweru
|
||||||
|
|
||||||
|
Kontejnerizované zrcadlo ostré Medicus databáze (Firebird) na serveru **tower** (Unraid, 192.168.1.76).
|
||||||
|
Nahrazuje dosavadní restore na Windows VM **reporter** — tu lze po ověření na Firebird části vypnout.
|
||||||
|
|
||||||
|
## Proč
|
||||||
|
|
||||||
|
Všechny ostatní DB (MySQL, PostgreSQL, MongoDB, Redis) běží na toweru jako Docker.
|
||||||
|
Firebird sem logicky patří taky: jeden host, jeden režim záloh/monitoringu, žádná Windows VM navíc.
|
||||||
|
|
||||||
|
## Tok dat
|
||||||
|
|
||||||
|
```
|
||||||
|
Ordinace: gbak -> zip -> rsync na tower (~02:15)
|
||||||
|
Tower: /mnt/user/OrdinaceSynology/MedicusBackup/MEDICUS_RRMMDD_HHMM.zip (zalohy se HROMADI)
|
||||||
|
restore_medicus.sh (denne):
|
||||||
|
1) nejnovejsi MEDICUS_*.zip podle nazvu; pokud == last_restored.txt -> skip
|
||||||
|
2) pocka az velikost prestane rust (probiha-li jeste rsync) + overi unzip -t
|
||||||
|
3) unzip .fbk -> gbak -r do medicus_new.fdb -> stop+swap+start kontejneru
|
||||||
|
4) zapise marker (last_restored.txt)
|
||||||
|
5) GFS retence zaloh (prune_backups.sh)
|
||||||
|
Kontejner: firebird-medicus -> serve tower:3050 /firebird/data/medicus.fdb
|
||||||
|
```
|
||||||
|
|
||||||
|
Zdrojová DB: **Firebird 2.5.7**, ODS 11.2, dialect 3, page size 8192.
|
||||||
|
Image: `jacobalberty/firebird:2.5-ss` = **Firebird 2.5.9** (restore 2.5.7 → 2.5.9 v rámci řady OK).
|
||||||
|
|
||||||
|
## Soubory
|
||||||
|
|
||||||
|
| Soubor | Popis |
|
||||||
|
|--------|-------|
|
||||||
|
| `firebird_create.sh` | Jednorázové vytvoření / znovuvytvoření kontejneru |
|
||||||
|
| `restore_medicus.sh` | Denní rutina: obnova z nejnovější zálohy + retence (cron) |
|
||||||
|
| `prune_backups.sh` | GFS retence záloh (volá se z restore; lze i samostatně) |
|
||||||
|
| `verify_firebird.sh` | Kontrola: verze enginu, ODS, počet pacientů |
|
||||||
|
| `last_restored.txt` | Marker poslední úspěšně restorované zálohy (vzniká za běhu) |
|
||||||
|
| `restore.log` | Log denních běhů |
|
||||||
|
|
||||||
|
## Umístění na toweru
|
||||||
|
|
||||||
|
- Skripty: `/mnt/user/Scripts/MedicusFirebird/`
|
||||||
|
- Data kontejneru: `/mnt/user/appdata/firebird-medicus/fb` (→ `/firebird`, soubor `data/medicus.fdb`)
|
||||||
|
- Rozbalovací prostor pro `.fbk`: `/mnt/user/appdata/firebird-medicus/work` (→ `/work`)
|
||||||
|
|
||||||
|
## Kontejner
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d --name firebird-medicus --restart unless-stopped \
|
||||||
|
-p 3050:3050 -e ISC_PASSWORD=masterkey -e TZ=Europe/Prague \
|
||||||
|
-v /mnt/user/appdata/firebird-medicus/fb:/firebird \
|
||||||
|
-v /mnt/user/appdata/firebird-medicus/work:/work \
|
||||||
|
jacobalberty/firebird:2.5-ss
|
||||||
|
```
|
||||||
|
|
||||||
|
Pozn.: `gbak`/`isql` jsou v `/usr/local/firebird/bin/` (nejsou v PATH → volat plnou cestou).
|
||||||
|
Hesla jsou v `security2.fdb` (nastaveno přes `ISC_PASSWORD`), ne v `medicus.fdb` — restore dat heslo nemění.
|
||||||
|
|
||||||
|
## Robustnost restoru
|
||||||
|
|
||||||
|
Zálohy se v adresáři **hromadí** a nejnovější se může právě **přenášet přes rsync**, proto:
|
||||||
|
- výběr nejnovější podle **názvu** (`RRMMDD_HHMM` → lexikálně = chronologicky)
|
||||||
|
- **stav** v `last_restored.txt` → když není nic novějšího, nic se nedělá
|
||||||
|
- **čeká na dokončení přenosu** (velikost se ustálí) a ověří integritu `unzip -t` — nikdy nezpracuje nekompletní soubor
|
||||||
|
- marker se zapíše **až po úspěšném** restoru; zámek (`flock`) proti souběhu
|
||||||
|
|
||||||
|
## Retence záloh (GFS, sekvenční, počítaná)
|
||||||
|
|
||||||
|
`prune_backups.sh` drží v adresáři záloh schéma:
|
||||||
|
1. **30 posledních dní** — nech všechny denní
|
||||||
|
2. **pak 8 týdnů** — z každého ISO-týdne 1× (nejnovější = konec týdne)
|
||||||
|
3. **pak 12 měsíců** — z každého měsíce 1× (nejnovější)
|
||||||
|
4. starší → smazat
|
||||||
|
|
||||||
|
Datum se čte **z názvu** (ne mtime). Neparsovatelné názvy se nikdy nemažou.
|
||||||
|
Bezpečnostní přepínač `DRY_RUN=1` (jen výpis) / `DRY_RUN=0` (maže). V denní rutině řízeno `RETENTION_DRYRUN`
|
||||||
|
v `restore_medicus.sh` (ostré = 0).
|
||||||
|
|
||||||
|
## Připojení klientů (fdb / DSN)
|
||||||
|
|
||||||
|
```
|
||||||
|
192.168.1.76:/firebird/data/medicus.fdb SYSDBA / masterkey, charset win1250
|
||||||
|
# nebo: tower:/firebird/data/medicus.fdb
|
||||||
|
```
|
||||||
|
|
||||||
|
V `Knihovny/medicus_db.py` je odpovídající záznam v `dsn_map` (klíč `TOWER`).
|
||||||
|
Cutover skriptů/MCP z reporteru (2.5.7) na tower (2.5.9) = otevřené rozhodnutí.
|
||||||
|
|
||||||
|
## Cron (na toweru)
|
||||||
|
|
||||||
|
Záloha přistává ~02:15; denní rutina poté. Plánovat přes **User Scripts plugin**
|
||||||
|
(vzor: `PostgreSQLRestoreFromBackup`), spouštět:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/user/Scripts/MedicusFirebird/restore_medicus.sh # napr. 06:30 denne
|
||||||
|
```
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Vytvori (nebo znovuvytvori) Firebird 2.5 kontejner = zrcadlo Medicus DB na toweru.
|
||||||
|
# Spousti se jednorazove pri zakladani / zmene konfigurace.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME=firebird-medicus
|
||||||
|
IMAGE=jacobalberty/firebird:2.5-ss
|
||||||
|
APPDATA=/mnt/user/appdata/firebird-medicus
|
||||||
|
FBDIR="$APPDATA/fb" # -> /firebird (data, system, security2.fdb)
|
||||||
|
WORKDIR="$APPDATA/work" # -> /work (sem se rozbaluje .fbk pred restorem)
|
||||||
|
PASS=masterkey
|
||||||
|
|
||||||
|
mkdir -p "$FBDIR" "$WORKDIR"
|
||||||
|
|
||||||
|
# odstran stary kontejner, pokud existuje (data v appdata zustanou)
|
||||||
|
docker rm -f "$NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3050:3050 \
|
||||||
|
-e ISC_PASSWORD="$PASS" \
|
||||||
|
-e TZ=Europe/Prague \
|
||||||
|
-v "$FBDIR":/firebird \
|
||||||
|
-v "$WORKDIR":/work \
|
||||||
|
"$IMAGE"
|
||||||
|
|
||||||
|
echo "Kontejner $NAME vytvoren. Cekam na start serveru..."
|
||||||
|
sleep 10
|
||||||
|
docker ps --filter "name=$NAME" --format "{{.Names}} {{.Status}} {{.Ports}}"
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GFS retence PLNYCH zaloh Medicus (kazda zaloha je kompletni -> mazani ostatnich je bezpecne).
|
||||||
|
#
|
||||||
|
# SEKVENCNI, POCITANE tiery (jdou ZA sebou, neprekryvaji se), od nejnovejsiho zpet:
|
||||||
|
# 1) DENNI : poslednich 30 dni -> nech VSECHNY
|
||||||
|
# 2) TYDENNI : pak presne 8 tydnu dozadu -> z kazdeho ISO-tydne 1x (nejnovejsi = konec tydne)
|
||||||
|
# 3) MESICNI : pak presne 12 mesicu dozadu -> z kazdeho mesice 1x (nejnovejsi)
|
||||||
|
# 4) starsi : smazat
|
||||||
|
# Reference "ted" = datum NEJNOVEJSI zalohy. Datum se cte Z NAZVU MEDICUS_RRMMDD_HHMM.zip.
|
||||||
|
#
|
||||||
|
# BEZPECNOST: DRY_RUN=1 (default) jen vypisuje, NIC nemaze. DRY_RUN=0 skutecne maze.
|
||||||
|
# Neznamy/neparsovatelny nazev se NIKDY nemaze.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/mnt/user/OrdinaceSynology/MedicusBackup}"
|
||||||
|
DAILY_DAYS="${DAILY_DAYS:-30}"
|
||||||
|
WEEKLY_WEEKS="${WEEKLY_WEEKS:-8}"
|
||||||
|
MONTHLY_MONTHS="${MONTHLY_MONTHS:-12}"
|
||||||
|
DRY_RUN="${DRY_RUN:-1}"
|
||||||
|
|
||||||
|
date_from_name() { local d="${1#MEDICUS_}"; d="${d:0:6}"; echo "20${d:0:2}-${d:2:2}-${d:4:2}"; }
|
||||||
|
|
||||||
|
mapfile -t FILES < <(cd "$BACKUP_DIR" && ls -1 MEDICUS_*.zip 2>/dev/null | sort -r) # nejnovejsi prvni
|
||||||
|
[ "${#FILES[@]}" -eq 0 ] && { echo "Zadne zalohy v $BACKUP_DIR"; exit 0; }
|
||||||
|
|
||||||
|
REF=$(date_from_name "${FILES[0]}")
|
||||||
|
date -d "$REF" >/dev/null 2>&1 || { echo "CHYBA: nelze precist datum z ${FILES[0]}"; exit 1; }
|
||||||
|
D_CUT=$(date -d "$REF -${DAILY_DAYS} days" +%F)
|
||||||
|
echo "REF=$REF denni>=$D_CUT, pak ${WEEKLY_WEEKS}x tydenni, pak ${MONTHLY_MONTHS}x mesicni (starsi smazat)"
|
||||||
|
|
||||||
|
declare -A KEEP seen_week seen_month
|
||||||
|
dn=0; w=0; m=0
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
dt=$(date_from_name "$f")
|
||||||
|
if ! date -d "$dt" >/dev/null 2>&1; then KEEP[$f]="?"; continue; fi # neparsovatelne -> ponechat
|
||||||
|
if [[ ! "$dt" < "$D_CUT" ]]; then KEEP[$f]="d"; dn=$((dn+1)); continue; fi # 1) denni (30 dni)
|
||||||
|
if [ "$w" -lt "$WEEKLY_WEEKS" ]; then # 2) tydenni (8x)
|
||||||
|
wk=$(date -d "$dt" +%G-%V)
|
||||||
|
[ -z "${seen_week[$wk]:-}" ] && { seen_week[$wk]=1; w=$((w+1)); KEEP[$f]="w"; }
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ "$m" -lt "$MONTHLY_MONTHS" ]; then # 3) mesicni (12x)
|
||||||
|
mo=$(date -d "$dt" +%Y-%m)
|
||||||
|
[ -z "${seen_month[$mo]:-}" ] && { seen_month[$mo]=1; m=$((m+1)); KEEP[$f]="m"; }
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
mode="DRY-RUN (nic se nemaze)"; [ "$DRY_RUN" = "0" ] && mode="OSTRY (maze!)"
|
||||||
|
echo "=== GFS retence $mode | $BACKUP_DIR ==="
|
||||||
|
echo "schema: ${DAILY_DAYS}d / ${WEEKLY_WEEKS}t / ${MONTHLY_MONTHS}m | celkem: ${#FILES[@]} | ponechano: ${#KEEP[@]} (denni=$dn tydenni=$w mesicni=$m)"
|
||||||
|
|
||||||
|
del=0
|
||||||
|
for f in "${FILES[@]}"; do
|
||||||
|
if [ -n "${KEEP[$f]:-}" ]; then
|
||||||
|
printf ' KEEP [%s] %s\n' "${KEEP[$f]}" "$f"
|
||||||
|
else
|
||||||
|
printf ' DEL %s\n' "$f"; del=$((del+1))
|
||||||
|
[ "$DRY_RUN" = "0" ] && rm -f -- "$BACKUP_DIR/$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "=== ke smazani: $del ==="
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Denni obnova zrcadla Medicus DB z nejnovejsi gbak zalohy do Firebird kontejneru
|
||||||
|
# + GFS retence zaloh.
|
||||||
|
#
|
||||||
|
# Zalohy se v adresari HROMADI a nejnovejsi se muze prave PRENASET pres rsync. Proto:
|
||||||
|
# - vybira nejnovejsi MEDICUS_*.zip podle nazvu (RRMMDD_HHMM -> lexikalne = chronologicky)
|
||||||
|
# - pamatuje si posledni uspesne restorovanou (last_restored.txt) -> neni-li nic novejsiho, konci
|
||||||
|
# - ceka, az velikost prestane rust (probiha-li rsync), a overi integritu (unzip -t),
|
||||||
|
# teprve pak restoruje -> nikdy nezpracuje nekompletni prenos
|
||||||
|
# - marker se zapise az PO uspesnem restoru
|
||||||
|
# - na konci spusti GFS retenci zaloh (prune_backups.sh)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME=firebird-medicus
|
||||||
|
APPDATA=/mnt/user/appdata/firebird-medicus
|
||||||
|
DATA="$APPDATA/fb/data"
|
||||||
|
WORK="$APPDATA/work"
|
||||||
|
BACKUP_DIR=/mnt/user/OrdinaceSynology/MedicusBackup
|
||||||
|
GBAK=/usr/local/firebird/bin/gbak
|
||||||
|
PASS=masterkey
|
||||||
|
BASE_DIR=/mnt/user/Scripts/MedicusFirebird
|
||||||
|
STATE="$BASE_DIR/last_restored.txt"
|
||||||
|
LOG="$BASE_DIR/restore.log"
|
||||||
|
|
||||||
|
# Retence: ostra (maze dle GFS 30d/8t/12m). Pro testovaci beh prepnout na 1 (jen vypis).
|
||||||
|
RETENTION_DRYRUN="${RETENTION_DRYRUN:-0}"
|
||||||
|
|
||||||
|
exec >>"$LOG" 2>&1
|
||||||
|
exec 9>"$BASE_DIR/.restore.lock"
|
||||||
|
flock -n 9 || { echo "$(date '+%F %T') jiny restore uz bezi -> koncim."; exit 0; }
|
||||||
|
echo "===== $(date '+%F %T') restore start ====="
|
||||||
|
|
||||||
|
mkdir -p "$WORK" "$DATA"
|
||||||
|
|
||||||
|
# --- 1) nejnovejsi zaloha podle nazvu ---
|
||||||
|
ZIP=$(ls -1 "$BACKUP_DIR"/MEDICUS_*.zip 2>/dev/null | sort | tail -1 || true)
|
||||||
|
if [ -z "${ZIP:-}" ]; then echo "CHYBA: zadna zaloha v $BACKUP_DIR"; exit 1; fi
|
||||||
|
ZIP_BASE=$(basename "$ZIP")
|
||||||
|
LAST=$(cat "$STATE" 2>/dev/null || echo "")
|
||||||
|
echo "Nejnovejsi: $ZIP_BASE | posledni restorovana: ${LAST:-<zadna>}"
|
||||||
|
|
||||||
|
# --- 2) uz restorovana? ---
|
||||||
|
if [ "$ZIP_BASE" = "$LAST" ]; then
|
||||||
|
echo "Nic noveho -> restore preskocen."
|
||||||
|
else
|
||||||
|
# --- 3) pockej na dokonceni prenosu (velikost se ustali) ---
|
||||||
|
prev=-1; stable=0
|
||||||
|
for i in $(seq 1 80); do # max ~20 min cekani na rsync
|
||||||
|
cur=$(stat -c %s "$ZIP" 2>/dev/null || echo 0)
|
||||||
|
if [ "$cur" = "$prev" ] && [ "$cur" -gt 0 ]; then stable=1; break; fi
|
||||||
|
echo " ...$ZIP_BASE = $cur B, cekam na ustaleni"
|
||||||
|
prev=$cur; sleep 15
|
||||||
|
done
|
||||||
|
[ "$stable" = "1" ] || { echo "CHYBA: $ZIP_BASE se stale meni -> koncim (priste)."; exit 1; }
|
||||||
|
|
||||||
|
# --- 4) integrita (nekompletni/poskozeny zip neprojde) ---
|
||||||
|
echo "unzip -t ..."
|
||||||
|
unzip -tqq "$ZIP" || { echo "CHYBA: $ZIP_BASE neprosel unzip -t -> koncim."; exit 1; }
|
||||||
|
|
||||||
|
# --- 5) rozbaleni .fbk ---
|
||||||
|
rm -f "$WORK"/*.fbk
|
||||||
|
unzip -o "$ZIP" -d "$WORK" >/dev/null
|
||||||
|
FBK=$(ls -1t "$WORK"/*.fbk | head -1)
|
||||||
|
FBK_BASE=$(basename "$FBK")
|
||||||
|
echo "FBK: $FBK_BASE ($(du -h "$FBK" | cut -f1))"
|
||||||
|
|
||||||
|
# --- 6) restore pres bezici server do noveho souboru ---
|
||||||
|
docker start "$NAME" >/dev/null 2>&1 || true
|
||||||
|
sleep 8
|
||||||
|
echo "gbak restore -> medicus_new.fdb ..."
|
||||||
|
docker exec "$NAME" rm -f /firebird/data/medicus_new.fdb
|
||||||
|
docker exec "$NAME" "$GBAK" -r -p 8192 -user SYSDBA -password "$PASS" \
|
||||||
|
"/work/$FBK_BASE" "localhost:/firebird/data/medicus_new.fdb"
|
||||||
|
|
||||||
|
# --- 7) atomicky swap + restart ---
|
||||||
|
echo "swap + restart ..."
|
||||||
|
docker stop "$NAME" >/dev/null
|
||||||
|
mv -f "$DATA/medicus_new.fdb" "$DATA/medicus.fdb"
|
||||||
|
docker start "$NAME" >/dev/null
|
||||||
|
sleep 8
|
||||||
|
rm -f "$WORK"/*.fbk
|
||||||
|
|
||||||
|
# --- 8) marker az po uspechu ---
|
||||||
|
echo "$ZIP_BASE" > "$STATE"
|
||||||
|
echo "restore OK: $ZIP_BASE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 9) GFS retence zaloh ---
|
||||||
|
echo "--- retence zaloh (DRY_RUN=$RETENTION_DRYRUN) ---"
|
||||||
|
DRY_RUN="$RETENTION_DRYRUN" "$BASE_DIR/prune_backups.sh" || echo "VAROVANI: prune_backups.sh selhal"
|
||||||
|
|
||||||
|
echo "===== $(date '+%F %T') hotovo ====="
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Rychla kontrola obnoveneho zrcadla: verze enginu, ODS, pocet pacientu.
|
||||||
|
set -euo pipefail
|
||||||
|
NAME=firebird-medicus
|
||||||
|
PASS=masterkey
|
||||||
|
ISQL=/usr/local/firebird/bin/isql
|
||||||
|
DB=localhost:/firebird/data/medicus.fdb
|
||||||
|
|
||||||
|
docker exec -i "$NAME" "$ISQL" -user SYSDBA -password "$PASS" "$DB" <<'SQL'
|
||||||
|
SELECT rdb$get_context('SYSTEM','ENGINE_VERSION') AS ENGINE FROM rdb$database;
|
||||||
|
SELECT MON$ODS_MAJOR AS ODS_MAJOR, MON$ODS_MINOR AS ODS_MINOR, MON$PAGE_SIZE AS PAGE_SIZE FROM MON$DATABASE;
|
||||||
|
SELECT COUNT(*) AS PACIENTU FROM KAR;
|
||||||
|
QUIT;
|
||||||
|
SQL
|
||||||
+22
-10
@@ -4,18 +4,22 @@ Hledá ve schránce **ordinace@buzalkova.cz** e-maily, kde pacient žádá
|
|||||||
o předepsání léku (recept), vytěžuje pacienta + požadované léky a pacienta
|
o předepsání léku (recept), vytěžuje pacienta + požadované léky a pacienta
|
||||||
ověřuje v kartotéce Medicusu.
|
ověřuje v kartotéce Medicusu.
|
||||||
|
|
||||||
## Stav: testovací režim (read-only)
|
## Stav: nasazeno na toweru (produkce), DELTA režim
|
||||||
|
|
||||||
`recepty_agent.py` zatím jen načte `NEWEST_N` (= 5) nejnovějších mailů
|
`recepty_agent.py` zpracuje **všechny nové maily od posledního zpracovaného**
|
||||||
z Inboxu, klasifikuje je Claude modelem a vypíše report do konzole
|
(vodoznak), klasifikuje Claude modelem, identifikuje pacienta a u vysoké jistoty
|
||||||
a `_log_recepty.txt`. **Ve schránce nic nemění** (žádné kategorie,
|
založí požadavek v Medeviu, jinak dá dotaz do fronty (Telegram). Označuje maily
|
||||||
přesuny, odpovědi), žádný `state.json`.
|
kategoriemi. Běží na toweru — viz „Nasazení na tower" níže.
|
||||||
|
|
||||||
## Tok
|
## Tok
|
||||||
|
|
||||||
1. **Graph API** — `newest_inbox_messages()`: N nejnovějších mailů z Inboxu,
|
1. **Graph API — DELTA** `nove_inbox_messages(mailbox, since_iso)`: všechny maily
|
||||||
bez filtru na přílohy (`$orderby receivedDateTime desc` — bez filtru
|
s `receivedDateTime gt vodoznak`, řazeno vzestupně, stránkováno (max
|
||||||
funguje, na rozdíl od kombinace s `hasAttachments`, viz EmailAgent).
|
`MAX_PER_RUN`=200/běh). Vodoznak = `_last_processed.txt` (2 řádky: čas + ID
|
||||||
|
posl. mailu). POZOR: Graph má sub-sekundovou přesnost, ale `receivedDateTime`
|
||||||
|
se zobrazuje oříznutě na sekundy → `gt` vrací i hraniční už zpracovaný mail;
|
||||||
|
ten se odfiltruje podle uloženého ID. První běh (vodoznak chybí) jen nastaví
|
||||||
|
vodoznak na nejnovější mail a od příště jede dopředně (historie se nedohání).
|
||||||
2. **AI klasifikace + vytěžení (Claude `claude-haiku-4-5`)** — pro každý mail
|
2. **AI klasifikace + vytěžení (Claude `claude-haiku-4-5`)** — pro každý mail
|
||||||
JSON: `je_zadost_o_recept`, `pacient` (může se lišit od odesílatele —
|
JSON: `je_zadost_o_recept`, `pacient` (může se lišit od odesílatele —
|
||||||
příbuzní píší za pacienta), `rodne_cislo` (přesně jak je v textu),
|
příbuzní píší za pacienta), `rodne_cislo` (přesně jak je v textu),
|
||||||
@@ -162,11 +166,19 @@ založil „Recept na léky" správnému pacientovi (dotazník Název léků + P
|
|||||||
označené maily přeskočí (ještě před AI klasifikací). Maily čekající na
|
označené maily přeskočí (ještě před AI klasifikací). Maily čekající na
|
||||||
odpověď přes Telegram se přeskočí podle `recept_pending.je_mail_pending`
|
odpověď přes Telegram se přeskočí podle `recept_pending.je_mail_pending`
|
||||||
(znovu se neptá).
|
(znovu se neptá).
|
||||||
- Pozor: agent čte jen `NEWEST_N` (5) nejnovějších mailů — hloub do inboxu
|
- DELTA režim: zpracuje vše po vodoznaku (ne jen N nejnovějších). Strop
|
||||||
nejde. Když je všech 5 nejnovějších označených, neudělá nic.
|
`MAX_PER_RUN`=200/běh (kdyby byl vodoznak hodně zpět — zbytek dobere další běh).
|
||||||
|
Vodoznak lze ručně posunout úpravou `_last_processed.txt` (např. backfill).
|
||||||
|
|
||||||
## Spuštění
|
## Spuštění
|
||||||
|
|
||||||
|
Vývoj (notebook/Z230):
|
||||||
```powershell
|
```powershell
|
||||||
python U:\ordinaceprojekt\OrdinaceAgentEmail\recepty_agent.py
|
python U:\ordinaceprojekt\OrdinaceAgentEmail\recepty_agent.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Produkce (tower, python-runner) — viz „Nasazení na tower":
|
||||||
|
```
|
||||||
|
docker exec -e PYTHONIOENCODING=utf-8 -e MEDICUS_FDB_DSN=192.168.1.76:/firebird/data/medicus.fdb \
|
||||||
|
python-runner python /scripts/OrdinaceReceptAgent/OrdinaceAgentEmail/recepty_agent.py
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
recept_dialog.py — formátování otázky do Telegramu a parsování odpovědi člověka.
|
||||||
|
Čisté funkce bez vedlejších efektů (snadno testovatelné).
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
|
||||||
|
def _bez_diakritiky(s: str) -> str:
|
||||||
|
s = unicodedata.normalize("NFKD", s or "")
|
||||||
|
return "".join(c for c in s if not unicodedata.combining(c))
|
||||||
|
|
||||||
|
|
||||||
|
def format_otazka(rec: dict) -> str:
|
||||||
|
"""Sestaví text otázky do Telegramu z pending záznamu."""
|
||||||
|
sk = rec.get("skore")
|
||||||
|
lines = ["🟡 Žádost o recept — nejistá identifikace"
|
||||||
|
+ (f" (jistota {sk}/100)" if sk is not None else "")]
|
||||||
|
if rec.get("sender"):
|
||||||
|
lines.append(f"Od: {rec['sender']}")
|
||||||
|
if rec.get("email_subject"):
|
||||||
|
lines.append(f"Předmět: {rec['email_subject']}")
|
||||||
|
if rec.get("leky_str"):
|
||||||
|
lines.append(f"Léky: {rec['leky_str']}")
|
||||||
|
if rec.get("duvody"):
|
||||||
|
lines.append("Proč nejisté: " + "; ".join(rec["duvody"]))
|
||||||
|
|
||||||
|
kand = rec.get("kandidati") or []
|
||||||
|
if kand:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Kandidáti:")
|
||||||
|
for i, k in enumerate(kand, 1):
|
||||||
|
lines.append(
|
||||||
|
f" {i}) {k.get('prijmeni', '')} {k.get('jmeno', '')}"
|
||||||
|
f", RČ {k.get('rc', '?')}, nar. {k.get('datnar', '?')}"
|
||||||
|
f", poj. {k.get('poj', '?')}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("(žádný kandidát v kartotéce)")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Odpověz jako reply na tuto zprávu:")
|
||||||
|
lines.append("• RČ správného pacienta (definitivní)")
|
||||||
|
if kand:
|
||||||
|
lines.append("• nebo číslo kandidáta (1, 2, …)")
|
||||||
|
lines.append("• nebo „ne“ = nezakládat")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
_SKIP = {
|
||||||
|
"ne", "nezakladat", "preskoc", "preskocit", "zahodit", "zahod",
|
||||||
|
"ignoruj", "nic", "stop", "nezaklada", "nezakladej",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_odpoved(text: str) -> dict:
|
||||||
|
"""Rozparsuje odpověď člověka.
|
||||||
|
|
||||||
|
Vrací dict:
|
||||||
|
{"akce": "rc", "rc": "7309208104"} – definitivní RČ pacienta
|
||||||
|
{"akce": "kandidat", "index": 1} – výběr kandidáta podle pořadí
|
||||||
|
{"akce": "preskoc"} – nezakládat
|
||||||
|
{"akce": "nejasne"} – nerozpoznáno
|
||||||
|
"""
|
||||||
|
t = (text or "").strip()
|
||||||
|
low = _bez_diakritiky(t).lower().strip().rstrip(".!")
|
||||||
|
if low in _SKIP:
|
||||||
|
return {"akce": "preskoc"}
|
||||||
|
|
||||||
|
digits = re.sub(r"\D", "", t)
|
||||||
|
if len(digits) in (9, 10):
|
||||||
|
return {"akce": "rc", "rc": digits}
|
||||||
|
|
||||||
|
m = re.fullmatch(r"\s*(\d{1,2})\s*", t)
|
||||||
|
if m:
|
||||||
|
return {"akce": "kandidat", "index": int(m.group(1))}
|
||||||
|
|
||||||
|
return {"akce": "nejasne"}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
recept_pending.py — fronta "čekajících" žádostí o recept, u kterých si agent
|
||||||
|
NENÍ jistý identifikací pacienta a potřebuje potvrzení člověka přes Telegram.
|
||||||
|
|
||||||
|
E-mailový agent sem zapíše záznam (stav 'ceka') a NIC dalšího nedělá.
|
||||||
|
Resolver (recept_resolver.py) záznamy bere, pošle otázku do Telegramu a podle
|
||||||
|
odpovědi člověka založí požadavek správnému pacientovi.
|
||||||
|
|
||||||
|
Úložiště: JSON soubor _pending_recepty.json vedle tohoto modulu (atomický zápis).
|
||||||
|
Stavy záznamu: 'ceka' → 'zalozeno' | 'preskoceno'.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STORE = Path(__file__).resolve().parent / "_pending_recepty.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load() -> list:
|
||||||
|
if not STORE.exists():
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return json.loads(STORE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _save(items: list) -> None:
|
||||||
|
tmp = tempfile.NamedTemporaryFile(
|
||||||
|
"w", encoding="utf-8", dir=str(STORE.parent), delete=False, suffix=".tmp"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
json.dump(items, tmp, ensure_ascii=False, indent=2)
|
||||||
|
tmp.flush()
|
||||||
|
os.fsync(tmp.fileno())
|
||||||
|
tmp.close()
|
||||||
|
os.replace(tmp.name, STORE)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
os.unlink(tmp.name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def pridej(*, email_message_id: str, email_subject: str = "", sender: str = "",
|
||||||
|
leky_str: str = "", pozn_str: str = "", skore=None,
|
||||||
|
duvody=None, kandidati=None) -> dict:
|
||||||
|
"""Přidá nový čekající dotaz. leky_str/pozn_str se použijí při pozdějším
|
||||||
|
založení požadavku, kandidáti se ukážou člověku v otázce."""
|
||||||
|
items = _load()
|
||||||
|
rec = {
|
||||||
|
"id": uuid.uuid4().hex,
|
||||||
|
"vytvoreno": datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"email_message_id": email_message_id,
|
||||||
|
"email_subject": email_subject,
|
||||||
|
"sender": sender,
|
||||||
|
"leky_str": leky_str,
|
||||||
|
"pozn_str": pozn_str,
|
||||||
|
"skore": skore,
|
||||||
|
"duvody": duvody or [],
|
||||||
|
"kandidati": kandidati or [],
|
||||||
|
"otazka_message_id": None, # vyplní resolver po odeslání otázky
|
||||||
|
"stav": "ceka", # ceka | zalozeno | preskoceno
|
||||||
|
"vysledek": None,
|
||||||
|
}
|
||||||
|
items.append(rec)
|
||||||
|
_save(items)
|
||||||
|
return rec
|
||||||
|
|
||||||
|
|
||||||
|
def cekajici() -> list:
|
||||||
|
return [r for r in _load() if r.get("stav") == "ceka"]
|
||||||
|
|
||||||
|
|
||||||
|
def cekajici_bez_otazky() -> list:
|
||||||
|
"""Záznamy, na které se ještě neposlala otázka do Telegramu."""
|
||||||
|
return [r for r in _load()
|
||||||
|
if r.get("stav") == "ceka" and not r.get("otazka_message_id")]
|
||||||
|
|
||||||
|
|
||||||
|
def je_mail_pending(email_message_id: str) -> bool:
|
||||||
|
return any(r.get("email_message_id") == email_message_id
|
||||||
|
and r.get("stav") == "ceka" for r in _load())
|
||||||
|
|
||||||
|
|
||||||
|
def najdi_dle_otazky(otazka_message_id) -> dict | None:
|
||||||
|
if otazka_message_id is None:
|
||||||
|
return None
|
||||||
|
for r in _load():
|
||||||
|
if r.get("otazka_message_id") == otazka_message_id:
|
||||||
|
return r
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def aktualizuj(rec_id: str, **fields) -> dict | None:
|
||||||
|
items = _load()
|
||||||
|
out = None
|
||||||
|
for r in items:
|
||||||
|
if r.get("id") == rec_id:
|
||||||
|
r.update(fields)
|
||||||
|
out = r
|
||||||
|
break
|
||||||
|
if out is not None:
|
||||||
|
_save(items)
|
||||||
|
return out
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
recept_resolver.py — vyřizuje "čekající" žádosti o recept přes Telegram.
|
||||||
|
|
||||||
|
Jediný proces, který mluví s Telegramem (kvůli getUpdates). Ve smyčce:
|
||||||
|
1) pošle otázky pro nové pending záznamy (které ještě otázku nemají),
|
||||||
|
2) long-polluje odpovědi; odpověď (reply na otázku) → podle obsahu:
|
||||||
|
• RČ (9–10 číslic) → definitivní volba pacienta → založí požadavek
|
||||||
|
• číslo kandidáta → vybere kandidáta → založí požadavek
|
||||||
|
• „ne“ → přeskočí (nezakládá)
|
||||||
|
Po založení označí původní e-mail kategorií ClaudeZpracovalRecept.
|
||||||
|
|
||||||
|
Telegram přenos je v recept_telegram.py (token/chat dodá uživatel).
|
||||||
|
Bez odpovědi záznam zůstává 'ceka' (čeká se libovolně dlouho).
|
||||||
|
|
||||||
|
Spuštění: python recept_resolver.py
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(HERE.parent / "EmailAgent"))
|
||||||
|
sys.path.insert(0, str(HERE.parent))
|
||||||
|
|
||||||
|
import recept_pending as PEND # noqa: E402
|
||||||
|
import recept_dialog as DLG # noqa: E402
|
||||||
|
import recept_telegram as TG # noqa: E402
|
||||||
|
import graph_mail # noqa: E402 (značení mailu)
|
||||||
|
import mcp_medevio as MED # noqa: E402 (zaloz_pozadavek_recept)
|
||||||
|
from Knihovny.mysql_db import connect_mysql # noqa: E402
|
||||||
|
|
||||||
|
MAILBOX = "ordinace@buzalkova.cz"
|
||||||
|
PROCESSED_CATEGORY = "ClaudeZpracovalRecept"
|
||||||
|
SINCE_FILE = HERE / "_resolver_since.txt" # poslední zpracované message_id
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_rc(s: str) -> str:
|
||||||
|
return re.sub(r"\D", "", s or "")
|
||||||
|
|
||||||
|
|
||||||
|
def najdi_pacienta_dle_rc(rc: str):
|
||||||
|
"""RČ → (uuid, jmeno, prijmeni) z medevio_pacient, nebo None."""
|
||||||
|
rc = _norm_rc(rc)
|
||||||
|
if not rc:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
conn = connect_mysql()
|
||||||
|
conn.ping(reconnect=True)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT patient_id, name, surname FROM medevio_pacient "
|
||||||
|
"WHERE REPLACE(identification_number,'/','') = %s LIMIT 1",
|
||||||
|
[rc],
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return row if row else None
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[uuid lookup chyba] {type(e).__name__}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_since():
|
||||||
|
try:
|
||||||
|
return int(SINCE_FILE.read_text(encoding="utf-8").strip())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _save_since(s: int) -> None:
|
||||||
|
SINCE_FILE.write_text(str(s), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def posli_nove_otazky() -> None:
|
||||||
|
"""Pošle otázku do Telegramu pro každý pending záznam bez otázky."""
|
||||||
|
for rec in PEND.cekajici_bez_otazky():
|
||||||
|
try:
|
||||||
|
mid = TG.posli_otazku(DLG.format_otazka(rec))
|
||||||
|
PEND.aktualizuj(rec["id"], otazka_message_id=mid)
|
||||||
|
log(f"otázka odeslána (pending {rec['id'][:8]}, msg {mid})")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"otázku nelze odeslat ({type(e).__name__}: {e}) — zkusím příště")
|
||||||
|
break # nejspíš výpadek / nenakonfigurováno → nech na příští kolo
|
||||||
|
|
||||||
|
|
||||||
|
def _zaloz_pro_rc(rec: dict, rc: str) -> None:
|
||||||
|
info = najdi_pacienta_dle_rc(rc)
|
||||||
|
if not info:
|
||||||
|
TG.posli_zpravu(f"⚠ RČ {rc} není v Medeviu — nezakládám. Zkus jiné RČ.")
|
||||||
|
return
|
||||||
|
uuid_, jmeno, prijmeni = info[0], info[1], info[2]
|
||||||
|
try:
|
||||||
|
res = MED.zaloz_pozadavek_recept(
|
||||||
|
uuid_, rec.get("leky_str", ""), rec.get("pozn_str", "")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
TG.posli_zpravu(f"❌ Chyba při zakládání: {type(e).__name__}: {e}")
|
||||||
|
return
|
||||||
|
PEND.aktualizuj(rec["id"], stav="zalozeno",
|
||||||
|
vysledek={"request_id": res["request_id"], "rc": rc,
|
||||||
|
"pacient": f"{prijmeni} {jmeno}"})
|
||||||
|
try:
|
||||||
|
graph_mail.add_category(MAILBOX, rec["email_message_id"], PROCESSED_CATEGORY)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[mail označení] {type(e).__name__}: {e}")
|
||||||
|
TG.posli_zpravu(f"✅ Založeno: {prijmeni} {jmeno} (RČ {rc}) — "
|
||||||
|
f"{rec.get('leky_str', '')}")
|
||||||
|
log(f"založeno {res['request_id']} pro {prijmeni} {jmeno}")
|
||||||
|
|
||||||
|
|
||||||
|
def zpracuj_odpoved(u: dict) -> None:
|
||||||
|
"""Zpracuje jednu příchozí Telegram zprávu."""
|
||||||
|
rid = u.get("reply_to_message_id")
|
||||||
|
rec = PEND.najdi_dle_otazky(rid) if rid else None
|
||||||
|
|
||||||
|
if rec is None:
|
||||||
|
cekaji = PEND.cekajici()
|
||||||
|
if len(cekaji) == 1:
|
||||||
|
rec = cekaji[0] # jediný čekající → ber to na něj
|
||||||
|
else:
|
||||||
|
if cekaji:
|
||||||
|
TG.posli_zpravu("Odpověz prosím jako reply na konkrétní dotaz "
|
||||||
|
"(čeká jich víc).")
|
||||||
|
return
|
||||||
|
|
||||||
|
if rec.get("stav") != "ceka":
|
||||||
|
return
|
||||||
|
|
||||||
|
d = DLG.parse_odpoved(u.get("text", ""))
|
||||||
|
if d["akce"] == "preskoc":
|
||||||
|
PEND.aktualizuj(rec["id"], stav="preskoceno")
|
||||||
|
TG.posli_zpravu("OK, nezakládám.")
|
||||||
|
elif d["akce"] == "rc":
|
||||||
|
_zaloz_pro_rc(rec, d["rc"])
|
||||||
|
elif d["akce"] == "kandidat":
|
||||||
|
kand = rec.get("kandidati") or []
|
||||||
|
i = d["index"] - 1
|
||||||
|
if 0 <= i < len(kand):
|
||||||
|
_zaloz_pro_rc(rec, kand[i].get("rc", ""))
|
||||||
|
else:
|
||||||
|
TG.posli_zpravu(f"Kandidát {d['index']} neexistuje (mám {len(kand)}).")
|
||||||
|
else:
|
||||||
|
TG.posli_zpravu("Nerozumím. Pošli RČ pacienta, číslo kandidáta, nebo „ne“.")
|
||||||
|
|
||||||
|
|
||||||
|
def smycka(poll_s: int = 5) -> None:
|
||||||
|
try:
|
||||||
|
TG.priprav() # naprimuj entitu Vlada (jinak fresh session spadne)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[priprav] {type(e).__name__}: {e}")
|
||||||
|
since = _load_since()
|
||||||
|
if since is None:
|
||||||
|
# první start — vezmi aktuální stav jako základ, starou historii ignoruj
|
||||||
|
since = TG.baseline_since()
|
||||||
|
_save_since(since)
|
||||||
|
log(f"baseline since_id={since}")
|
||||||
|
log("Resolver běží (Ctrl+C ukončí).")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
posli_nove_otazky()
|
||||||
|
nove, since = TG.nacti_odpovedi(since)
|
||||||
|
for u in nove:
|
||||||
|
zpracuj_odpoved(u)
|
||||||
|
_save_since(since)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log("Konec.")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[smyčka] {type(e).__name__}: {e}")
|
||||||
|
time.sleep(poll_s)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
smycka()
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
recept_telegram.py — Telegram přenos pro recept-resolver.
|
||||||
|
|
||||||
|
Používá USER účet agenta (Claude Buzalka @vlado_claude_agent) přes Telethon
|
||||||
|
z Knihovny/telegram_user.py — VLASTNÍ session "recepty" (per-agent autorizace).
|
||||||
|
Odpovědi se párují přes Telegram reply (reply_to_msg_id), takže víc agentů na
|
||||||
|
témž účtu se nepoplete a každý si hlídá jen své odpovědi.
|
||||||
|
|
||||||
|
JEDNORÁZOVÝ KROK (dělá uživatel v terminálu — čeká na SMS kód):
|
||||||
|
python -m Knihovny.telegram_user login --jako recepty
|
||||||
|
|
||||||
|
Konfigurace (Medevio/.env): TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_PHONE
|
||||||
|
(viz Trilium „2026-06-14 Telegram — bot, user účet agenta a MCP server").
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from Knihovny.telegram_user import posli_jako_ja, precti_zpravy, _new_client # noqa: E402
|
||||||
|
|
||||||
|
# Vlastní session recept-agenta (jedno přihlášení: login --jako recepty).
|
||||||
|
SESSION = "recepty"
|
||||||
|
|
||||||
|
# Komu agent píše = Vladův hlavní účet (z Trilium / TELEGRAM_CHAT_ID).
|
||||||
|
VLADO_UID = int(os.environ.get("TELEGRAM_CHAT_ID", "6639316354"))
|
||||||
|
|
||||||
|
|
||||||
|
def priprav() -> None:
|
||||||
|
"""Načte dialogy → do session cache se uloží entita Vlada. Nová session by ji
|
||||||
|
jinak neznala a posílání by spadlo na 'Could not find the input entity'.
|
||||||
|
Volá se jednou při startu resolveru (idempotentní, levné)."""
|
||||||
|
with _new_client(SESSION) as client:
|
||||||
|
if client.is_user_authorized():
|
||||||
|
client.get_dialogs(limit=50)
|
||||||
|
|
||||||
|
|
||||||
|
def posli_otazku(text: str) -> int:
|
||||||
|
"""Pošle otázku Vladovi z účtu agenta. Vrátí message_id (pro párování reply)."""
|
||||||
|
msg = posli_jako_ja(VLADO_UID, text, session=SESSION)
|
||||||
|
return msg.id
|
||||||
|
|
||||||
|
|
||||||
|
def posli_zpravu(text: str) -> None:
|
||||||
|
"""Pošle prostou zprávu (potvrzení / chybu)."""
|
||||||
|
posli_jako_ja(VLADO_UID, text, session=SESSION)
|
||||||
|
|
||||||
|
|
||||||
|
def baseline_since() -> int:
|
||||||
|
"""Aktuální nejvyšší message_id v chatu — výchozí bod při prvním startu
|
||||||
|
(aby resolver nezpracoval starou historii)."""
|
||||||
|
zpravy = precti_zpravy(VLADO_UID, limit=1, session=SESSION)
|
||||||
|
return max((z["id"] for z in zpravy), default=0)
|
||||||
|
|
||||||
|
|
||||||
|
def nacti_odpovedi(since_id: int = 0, limit: int = 50):
|
||||||
|
"""Vrátí (nove_odpovedi, novy_since_id).
|
||||||
|
|
||||||
|
Bere jen PŘÍCHOZÍ zprávy (ne naše) s id > since_id. Každá:
|
||||||
|
{message_id, text, reply_to_message_id}
|
||||||
|
"""
|
||||||
|
zpravy = precti_zpravy(VLADO_UID, limit=limit, session=SESSION)
|
||||||
|
out = []
|
||||||
|
new_since = since_id
|
||||||
|
for z in zpravy:
|
||||||
|
mid = z["id"]
|
||||||
|
if mid > new_since:
|
||||||
|
new_since = mid
|
||||||
|
if mid > since_id and not z["odeslal_ja"]:
|
||||||
|
out.append({
|
||||||
|
"message_id": mid,
|
||||||
|
"text": z["text"],
|
||||||
|
"reply_to_message_id": z["reply_na"],
|
||||||
|
})
|
||||||
|
out.sort(key=lambda x: x["message_id"]) # od nejstarší
|
||||||
|
return out, new_since
|
||||||
@@ -26,7 +26,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from datetime import date, timedelta
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -52,8 +52,11 @@ import recept_pending as _pending # noqa: E402 fronta dotazů (nejistá identi
|
|||||||
# =========================
|
# =========================
|
||||||
MAILBOX = "ordinace@buzalkova.cz"
|
MAILBOX = "ordinace@buzalkova.cz"
|
||||||
|
|
||||||
# Kolik nejnovějších mailů z Inboxu zpracovat (testovací režim).
|
# DELTA REŽIM: zpracují se všechny maily s receivedDateTime > vodoznak
|
||||||
NEWEST_N = 5
|
# (čas posledního zpracovaného mailu, uložen v _last_processed.txt). Při prvním
|
||||||
|
# běhu (vodoznak chybí) se vodoznak nastaví na nejnovější mail v Inboxu a od
|
||||||
|
# příště se zpracuje jen to, co přijde POTÉ (historie se nedohání).
|
||||||
|
MAX_PER_RUN = 200 # pojistka: max mailů na jeden běh (kdyby byl vodoznak hodně zpět)
|
||||||
|
|
||||||
# Kategorie (štítek na mailu), kterou agent označí mail po úspěšném založení
|
# Kategorie (štítek na mailu), kterou agent označí mail po úspěšném založení
|
||||||
# požadavku v Medeviu. Při dalším běhu se takto označené maily přeskočí
|
# požadavku v Medeviu. Při dalším běhu se takto označené maily přeskočí
|
||||||
@@ -85,6 +88,7 @@ _cost = {"input_tokens": 0, "output_tokens": 0, "usd": 0.0, "calls": 0}
|
|||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
LOG_FILE = HERE / "_log_recepty.txt"
|
LOG_FILE = HERE / "_log_recepty.txt"
|
||||||
|
WATERMARK_FILE = HERE / "_last_processed.txt" # ISO čas (receivedDateTime) posledního zpracovaného mailu
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
@@ -112,18 +116,59 @@ def log(msg: str) -> None:
|
|||||||
# =========================
|
# =========================
|
||||||
# ČTENÍ MAILŮ (Graph, read-only)
|
# ČTENÍ MAILŮ (Graph, read-only)
|
||||||
# =========================
|
# =========================
|
||||||
def newest_inbox_messages(mailbox: str, n: int) -> list[dict]:
|
_SELECT = "id,subject,from,receivedDateTime,bodyPreview,body,categories"
|
||||||
"""N nejnovějších mailů z Inboxu (bez filtru na přílohy), tělo jako text."""
|
|
||||||
|
|
||||||
|
def _load_watermark() -> tuple[str | None, str | None]:
|
||||||
|
"""Vrátí (receivedDateTime, id) posledního zpracovaného mailu (řádek 1 a 2
|
||||||
|
v _last_processed.txt). id slouží k odfiltrování hraničního mailu, který se
|
||||||
|
kvůli sub-sekundové přesnosti vrací i při filtru `gt` na oříznutý čas."""
|
||||||
|
try:
|
||||||
|
lines = WATERMARK_FILE.read_text(encoding="utf-8").splitlines()
|
||||||
|
t = (lines[0].strip() if lines else "") or None
|
||||||
|
i = (lines[1].strip() if len(lines) > 1 else "") or None
|
||||||
|
return t, i
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _save_watermark(iso: str, msg_id: str = "") -> None:
|
||||||
|
WATERMARK_FILE.write_text(f"{iso}\n{msg_id}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def newest_received(mailbox: str) -> tuple[str, str]:
|
||||||
|
"""(receivedDateTime, id) nejnovějšího mailu v Inboxu — seed vodoznaku při
|
||||||
|
prvním běhu. Když je schránka prázdná, vrátí (aktuální UTC, '')."""
|
||||||
|
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
|
||||||
|
params = {"$orderby": "receivedDateTime desc", "$select": "id,receivedDateTime", "$top": 1}
|
||||||
|
r = requests.get(url, headers=graph_mail._headers(), params=params, timeout=60)
|
||||||
|
r.raise_for_status()
|
||||||
|
vals = r.json().get("value", [])
|
||||||
|
if vals:
|
||||||
|
return vals[0]["receivedDateTime"], vals[0]["id"]
|
||||||
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), ""
|
||||||
|
|
||||||
|
|
||||||
|
def nove_inbox_messages(mailbox: str, since_iso: str) -> list[dict]:
|
||||||
|
"""Všechny maily z Inboxu s receivedDateTime > since_iso, od NEJSTARŠÍHO.
|
||||||
|
Stránkuje přes @odata.nextLink, max MAX_PER_RUN za jeden běh."""
|
||||||
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
|
url = f"{graph_mail.GRAPH}/users/{mailbox}/mailFolders/inbox/messages"
|
||||||
params = {
|
params = {
|
||||||
"$orderby": "receivedDateTime desc",
|
"$filter": f"receivedDateTime gt {since_iso}",
|
||||||
"$select": "id,subject,from,receivedDateTime,bodyPreview,body,categories",
|
"$orderby": "receivedDateTime asc",
|
||||||
"$top": n,
|
"$select": _SELECT,
|
||||||
|
"$top": 50,
|
||||||
}
|
}
|
||||||
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
|
headers = {**graph_mail._headers(), "Prefer": 'outlook.body-content-type="text"'}
|
||||||
r = requests.get(url, headers=headers, params=params, timeout=60)
|
out: list[dict] = []
|
||||||
r.raise_for_status()
|
while url and len(out) < MAX_PER_RUN:
|
||||||
return r.json().get("value", [])[:n]
|
r = requests.get(url, headers=headers, params=params, timeout=60)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
out.extend(data.get("value", []))
|
||||||
|
url = data.get("@odata.nextLink")
|
||||||
|
params = None # nextLink už nese všechny parametry
|
||||||
|
return out[:MAX_PER_RUN]
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
@@ -644,7 +689,7 @@ def _medevio_find_patient(rc_normalized: str) -> str | None:
|
|||||||
# =========================
|
# =========================
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
log("\n" + "=" * 70)
|
log("\n" + "=" * 70)
|
||||||
log(f"START — schránka={MAILBOX}, {NEWEST_N} nejnovějších mailů")
|
log(f"START — schránka={MAILBOX}, DELTA režim (vše po posledním zpracovaném)")
|
||||||
log(f"REŽIM: zakládá požadavky v Medeviu; zpracované maily značí štítkem "
|
log(f"REŽIM: zakládá požadavky v Medeviu; zpracované maily značí štítkem "
|
||||||
f"'{PROCESSED_CATEGORY}' (a příště přeskakuje)")
|
f"'{PROCESSED_CATEGORY}' (a příště přeskakuje)")
|
||||||
|
|
||||||
@@ -655,8 +700,26 @@ def main() -> None:
|
|||||||
log(f"[POZOR] kategorii '{PROCESSED_CATEGORY}' nelze zajistit "
|
log(f"[POZOR] kategorii '{PROCESSED_CATEGORY}' nelze zajistit "
|
||||||
f"({type(e).__name__}: {e}) — chybí asi Mail.ReadWrite oprávnění")
|
f"({type(e).__name__}: {e}) — chybí asi Mail.ReadWrite oprávnění")
|
||||||
|
|
||||||
msgs = newest_inbox_messages(MAILBOX, NEWEST_N)
|
watermark, last_id = _load_watermark()
|
||||||
log(f"Načteno {len(msgs)} mailů.")
|
if watermark is None:
|
||||||
|
# první běh — neznáme „poslední zpracovaný"; nastav vodoznak na nejnovější
|
||||||
|
# mail a od příště zpracovávej jen to, co přijde potom (historie se nedohání).
|
||||||
|
watermark, seed_id = newest_received(MAILBOX)
|
||||||
|
_save_watermark(watermark, seed_id)
|
||||||
|
log(f"První běh — vodoznak nastaven na {watermark}. "
|
||||||
|
f"Příští běh zpracuje maily přijaté po tomto čase.")
|
||||||
|
return
|
||||||
|
|
||||||
|
msgs = nove_inbox_messages(MAILBOX, watermark)
|
||||||
|
# `gt` na oříznutý (sekundový) čas vrací i hraniční už zpracovaný mail
|
||||||
|
# (Graph má sub-sekundovou přesnost) → odfiltruj ho podle ID.
|
||||||
|
if last_id:
|
||||||
|
msgs = [m for m in msgs if m.get("id") != last_id]
|
||||||
|
cap = " (dosažen strop MAX_PER_RUN)" if len(msgs) >= MAX_PER_RUN else ""
|
||||||
|
log(f"Vodoznak: {watermark} → nových mailů: {len(msgs)}{cap}")
|
||||||
|
if not msgs:
|
||||||
|
log("Nic nového — končím.")
|
||||||
|
return
|
||||||
|
|
||||||
lookup = MedicusLookup()
|
lookup = MedicusLookup()
|
||||||
log(f"Medicus: kartotéka načtena ({len(lookup.patients)} pacientů, "
|
log(f"Medicus: kartotéka načtena ({len(lookup.patients)} pacientů, "
|
||||||
@@ -789,7 +852,9 @@ def main() -> None:
|
|||||||
log("")
|
log("")
|
||||||
|
|
||||||
lookup.close()
|
lookup.close()
|
||||||
log(f"HOTOVO: {len(msgs)} mailů, žádostí o recept: {requests_found}.")
|
_save_watermark(msgs[-1]["receivedDateTime"], msgs[-1].get("id", "")) # posun na nejnovější zpracovaný
|
||||||
|
log(f"HOTOVO: {len(msgs)} mailů, žádostí o recept: {requests_found}. "
|
||||||
|
f"Nový vodoznak: {msgs[-1]['receivedDateTime']}")
|
||||||
log(
|
log(
|
||||||
f"CENA AI: {_cost['calls']} volání, "
|
f"CENA AI: {_cost['calls']} volání, "
|
||||||
f"tokeny input={_cost['input_tokens']} output={_cost['output_tokens']}, "
|
f"tokeny input={_cost['input_tokens']} output={_cost['output_tokens']}, "
|
||||||
|
|||||||
+13
-1
@@ -30,7 +30,19 @@ from typing import Optional
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from dateutil import parser as dtparser, tz
|
from dateutil import parser as dtparser, tz
|
||||||
from mcp.server.fastmcp import FastMCP
|
try:
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
except Exception:
|
||||||
|
# Fallback, když balíček 'mcp' není nainstalován (např. python-runner na
|
||||||
|
# toweru): modul lze importovat kvůli funkcím (zaloz_pozadavek_recept…),
|
||||||
|
# jen MCP server běžet nemůže. @mcp.tool() se stane no-op průchodkou.
|
||||||
|
class FastMCP:
|
||||||
|
def __init__(self, *a, **k): pass
|
||||||
|
def tool(self, *a, **k):
|
||||||
|
def deco(f): return f
|
||||||
|
return deco
|
||||||
|
def run(self, *a, **k):
|
||||||
|
raise RuntimeError("Balíček 'mcp' není nainstalován — MCP server nelze spustit.")
|
||||||
|
|
||||||
# ── Všechny logy na stderr (stdout = JSON-RPC) ──────────────────────────────
|
# ── Všechny logy na stderr (stdout = JSON-RPC) ──────────────────────────────
|
||||||
def log(msg: str):
|
def log(msg: str):
|
||||||
|
|||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MCP server pro Telegram — FastMCP
|
||||||
|
Spustit: python mcp_telegram.py
|
||||||
|
|
||||||
|
Dává agentům nástroje pro komunikaci s Vladem přes Telegram.
|
||||||
|
|
||||||
|
Nástroje:
|
||||||
|
notifikace — pošle Vladovi zprávu přes bota (oznámení, nečeká na odpověď)
|
||||||
|
zeptej_se — pošle Vladovi otázku přes bota a POČKÁ na jeho odpověď
|
||||||
|
posli_jako_agent — pošle Vladovi zprávu z účtu agenta "Claude Buzalka" (persona)
|
||||||
|
zeptej_se_jako_agent — otázka z účtu agenta + čekání na odpověď
|
||||||
|
|
||||||
|
Dvě cesty:
|
||||||
|
• BOT (@Vlado_Claude_Bot) — bezpečné, bez rizika banu; doporučené pro notifikace/dotazy.
|
||||||
|
• USER účet (Claude Buzalka) — vystupuje jako "osoba"; má riziko banu (nový účet).
|
||||||
|
|
||||||
|
POZOR (bot): getUpdates smí pollovat jen JEDEN proces. Když běží víc agentů, kteří
|
||||||
|
naráz čekají na odpověď přes bota, kradou si zprávy. Pro souběžné dotazy víc agentů
|
||||||
|
použij user účet (každý agent vlastní session) — viz Knihovny/telegram_user.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
# Kořen projektu na sys.path, ať jdou importovat sdílené knihovny
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(BASE_DIR))
|
||||||
|
|
||||||
|
from Knihovny.telegram_notify import posli_telegram, zeptej_se_telegram
|
||||||
|
from Knihovny.telegram_user import posli_jako_ja, zeptej_se_jako
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str):
|
||||||
|
print(msg, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _v_threadu(fn, *args, **kwargs):
|
||||||
|
"""Spustí synchronní (telethon.sync) funkci ve vlastním vlákně s čistou
|
||||||
|
event-loop, aby nekolidovala s běžící async smyčkou FastMCP."""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
asyncio.set_event_loop(None)
|
||||||
|
|
||||||
|
|
||||||
|
# Vladův hlavní Telegram účet (user id == chat_id u bota)
|
||||||
|
VLADO_UID = 6639316354
|
||||||
|
|
||||||
|
mcp = FastMCP("telegram")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# BOT — bezpečná cesta
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
@mcp.tool()
|
||||||
|
def notifikace(text: str, tise: bool = False) -> dict:
|
||||||
|
"""Pošle Vladovi oznámení přes Telegram bota (nečeká na odpověď).
|
||||||
|
|
||||||
|
Použij pro informování o průběhu, dokončení úlohy, chybě apod.
|
||||||
|
|
||||||
|
:param text: text zprávy
|
||||||
|
:param tise: True = tichá zpráva bez zvuku/upozornění
|
||||||
|
:return: {"ok": True}
|
||||||
|
"""
|
||||||
|
posli_telegram(text, disable_notification=tise)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def zeptej_se(otazka: str, timeout_s: int = 180) -> dict:
|
||||||
|
"""Pošle Vladovi otázku přes bota a POČKÁ na jeho textovou odpověď.
|
||||||
|
|
||||||
|
Vhodné, když agent během běhu potřebuje rozhodnutí. Blokuje až `timeout_s` sekund.
|
||||||
|
|
||||||
|
:param otazka: text otázky (klidně s nabídkou možností, např. "(ano/ne)")
|
||||||
|
:param timeout_s: jak dlouho čekat na odpověď
|
||||||
|
:return: {"odpoved": <text>} nebo {"odpoved": None, "timeout": True}
|
||||||
|
"""
|
||||||
|
odp = zeptej_se_telegram(otazka, timeout=timeout_s)
|
||||||
|
if odp is None:
|
||||||
|
return {"odpoved": None, "timeout": True}
|
||||||
|
return {"odpoved": odp}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# USER účet "Claude Buzalka" — persona (riziko banu)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
@mcp.tool()
|
||||||
|
async def posli_jako_agent(text: str) -> dict:
|
||||||
|
"""Pošle Vladovi zprávu z účtu agenta "Claude Buzalka" (ne z bota).
|
||||||
|
|
||||||
|
:param text: text zprávy
|
||||||
|
:return: {"ok": True}
|
||||||
|
"""
|
||||||
|
await asyncio.to_thread(_v_threadu, posli_jako_ja, VLADO_UID, text)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def zeptej_se_jako_agent(otazka: str, agent: str = "Claude", timeout_s: int = 180) -> dict:
|
||||||
|
"""Pošle Vladovi otázku z účtu agenta a POČKÁ na odpověď (stačí běžná zpráva).
|
||||||
|
|
||||||
|
:param otazka: text otázky
|
||||||
|
:param agent: jméno agenta (štítek + session); výchozí "Claude"
|
||||||
|
:param timeout_s: jak dlouho čekat
|
||||||
|
:return: {"odpoved": <text>} nebo {"odpoved": None, "timeout": True}
|
||||||
|
"""
|
||||||
|
odp = await asyncio.to_thread(
|
||||||
|
_v_threadu, zeptej_se_jako, agent, otazka,
|
||||||
|
komu=VLADO_UID, session=None, timeout=timeout_s, vyzaduj_reply=False,
|
||||||
|
)
|
||||||
|
if odp is None:
|
||||||
|
return {"odpoved": None, "timeout": True}
|
||||||
|
return {"odpoved": odp}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
log("MCP Telegram server spuštěn (FastMCP)")
|
||||||
|
mcp.run()
|
||||||
Reference in New Issue
Block a user