Skill: youziyouzishu/tg-cs-agent (version 1.0.1)

--- name: tg-cs-agent description: Deploy and manage a Telegram customer service bot powered by Claude + RAG. Use when setting up a new CS bot, adding knowledge base docs, managing the bot lifecycle (start/stop/restart), or troubleshooting bot issues. Triggers on "customer service bot", "CS bot", "客服机器人", "telegram bot", "knowledge base". --- # Telegram Customer Service Agent RAG-powered Telegram customer service bot using Claude + Telethon + ChromaDB. ## Architecture - **Telegram**: Telethon userbot (not Bot API) — appears as a real user - **AI**: Claude via Anthropic API with RAG context from local markdown docs - **Knowledge Base**: ChromaDB + multilingual sentence-transformers embeddings - **Handoff**: Auto-escalation to human agent when AI can't help ## Prerequisites - Python 3.11+ - Telegram account with API credentials (get from https://my.telegram.org) - Anthropic API key - Telethon session (logged in via `tgctl-telethon login` or manual auth) ## Setup New Bot ### 1. Create project ```bash mkdir -p ~/tg-cs-bot && cd ~/tg-cs-bot cp -r <skill_dir>/scripts/*.py . pip install anthropic chromadb sentence-transformers python-dotenv telethon ``` ### 2. Configure environment Create `.env`: ``` ANTHROPIC_API_KEY=<key> ANTHROPIC_BASE_URL=https://api.anthropic.com TELEGRAM_API_ID=<id> TELEGRAM_API_HASH=<hash> TELEGRAM_PROFILE=default MODEL=claude-sonnet-4-20250514 KNOWLEDGE_DIR=docs MAX_HISTORY=20 HANDOFF_CHAT_ID=<admin_telegram_id_or_username> ``` ### 3. Add knowledge base Put markdown files in `docs/` directory. The bot splits by `##` headers and vectorizes with [REDACTED_SECRET] for multilingual support. ### 4. Login to Telegram (first time only) ```bash python3 -c " from telethon import TelegramClient import asyncio async def login(): c = TelegramClient('~/.tgctl-telethon/default/session', API_ID, 'API_HASH') await c.start() print('Logged in:', (await c.get_me()).first_name) asyncio.run(login()) " ``` ### 5. Run ```bash python3 main.py ``` ## Managing the Bot - **Start**: `python3 main.py` (or use the start.sh script) - **Stop**: Ctrl+C or kill the process - **Add knowledge**: Drop `.md` files in `docs/`, restart bot - **Clear user history**: User sends `/clear` - **Request human**: User sends `/human` ## Customization ### System Prompt Edit `config.py` → `__post_init__` → `self.system_prompt` to change: - Bot personality and tone - Language rules - Handoff trigger conditions - Platform-specific instructions ### Embedding Model In `knowledge.py`, change `model_name` for different language support: - [REDACTED_SECRET] — multilingual (recommended) - `all-MiniLM-L6-v2` — English only, faster - [REDACTED_SECRET] — Chinese optimized ### Handoff Behavior The bot adds `[HANDOFF]` to responses when it can't help. This triggers: 1. Notification to `HANDOFF_CHAT_ID` with user info 2. A message to the user that human support is coming ## File Structure ``` tg-cs-bot/ ├── main.py # Entry point, message routing ├── config.py # Config + system prompt ├── agent.py # Claude RAG agent ├── knowledge.py # ChromaDB knowledge base ├── telegram_client.py # Telethon wrapper ├── .env # Secrets (not committed) ├── docs/ # Knowledge base markdown files │ ├── platform.md │ ├── faq.md │ └── ... └── requirements.txt ``` ## Troubleshooting - **database is locked**: Kill all Python processes, delete `session.session-journal`, rebuild session with sqlite dump/restore - **Not logged in**: Run the login step again - **Poor RAG results**: Check embedding model matches your docs language, use multilingual model for mixed content - **Bot not responding**: Check `[READY] Listening for messages...` in logs, verify Telegram connection

path: references/seers-faq.md
content: # Seers Markets - 常见问题

## 什么是预测市场？

预测市场是一种让用户对未来事件结果进行预测和投注的平台。用户通过买入不同选项来表达自己的判断，如果预测正确则获得奖励。在 Seers，价格就是概率，无需赔率换算。

## Seers 是庄家吗？

不是。Seers 不是庄家，也不参与任何市场交易。平台不会操控市场结果，也不会从用户亏损中获利。Seers 的收入来源主要是平台基础交易手续费。

## 如何注册账号？

1. 访问 https://seers.market
2. 点击"登录/注册"
3. 连接你的 Web3 钱包（MetaMask、Phantom、OKX Wallet、Coinbase Wallet 或 WalletConnect 钱包）

## 如何获取试玩金？

新用户可以免费获取试玩金来体验预测市场，无需充值即可开始。在首页点击"免费获取试玩金玩预测"即可。

## 如何参与预测？

将资金投入对应选项的资金池中。事件结果裁定后，如果你选择的选项正确，将按照你在赢家资金池中的投注占比分享奖励（扣除平台手续费）。

## 两种参与模式

平台支持两种参与模式：

### 传统模式
无需庄家，所有用户互助投注，形成共享资金池。收益完全按占比计算，公平透明。

### 庄家模式（用户可自主创建）
事件创建者作为庄家（需支付保证金），用户投注庄家提供的池子。庄家可能承担部分风险，但也能获取超额收益。

## 收益怎么算？

如果你买的选项正确 → 按下注金额所在选项占比赢取其他所有错误选项中奖励总和的回报金额。

如果买错 → 下注金额归零。

示例（YES/NO 两个选项）：
你投入 100 USDT 买入 YES（假设投入后 YES 选项中累计投入金额为 1000 USDT）。如果事件结果为 YES，你将按自己在 YES 资金池中的投注占比，分享整个 NO 池的总奖励（扣除平台少量手续费后）。

收益多少取决于：
- YES 池子里所有正确投注的总金额（对手盘越多，池子越大，潜在收益越高）
- 你个人投注在池子里的比例（你投得越多，占比越高，分得越多）

## 如何创建预测事件？

用户可以自己创建预测事件，需要支付保证金。创建后其他用户可以参与该事件的交易。面向社区发起你关心的问题，由市场参与者共同决定概率。

## 支持哪些区块链？

当前支持：Solana、Polygon、Ethereum、BSC、Base、BTC L2（部分功能）。更多链上线中。

## 支持哪些钱包？

MetaMask、Phantom、OKX Wallet、Coinbase Wallet 以及所有 WalletConnect 钱包。

## 资金安全吗？

所有用户投注资金均部署并托管在智能合约中，保证资金安全透明、无法被平台随意动用。核心市场逻辑全部链上执行。

## 市场结算规则

所有预测市场在创建之初，均设有清晰明确的裁决规则与时间节点。结果依据公开、可验证的事实进行确认，裁决标准提前披露，避免歧义与争议。

## 仲裁机制

仲裁过程由链下官方裁决。支持证据提交与复核流程，提升公平性。

## 如何联系客服？

如需帮助，可以直接在 Telegram 上联系我们的客服机器人，我们提供 24/7 全天候服务。


path: references/seers-platform.md
content: # Seers Markets - 平台介绍

## 什么是 Seers？

Seers 是一个多链去中心化预测市场平台，允许用户：
- 创建预测事件（支付保证金）
- 参与事件交易（买卖 YES/NO 头寸）
- 提交裁定证据
- 基于事件结果获得奖励

Seers 利用资金池式的市场机制对未来事件进行预测和结算，用户投注形成不同选项的资金池，最终按照投注在正确选项资金池中的占比分配奖励。

## 平台网址

- 官方交易平台：https://seers.market
- 官方介绍网站：https://www.seersmarket.com

## Seers 不是庄家

Seers 不是庄家，也不参与任何市场交易。在 Seers 上：
- 用户创建事件
- 用户提供保证金
- 用户交易、做市、提供证据
- 市场的所有盈亏由用户承担

Seers 官方承担的只有：
- 仲裁 & 结果裁定
- 平台安全维护
- 链上基础设施

平台不会操控市场结果，也不会从用户亏损中获利。

## 平台收入来源

- 平台基础交易手续费
- 特定链的 Gas 返佣（如适用）
- API/开发者服务等衍生产品（未来）

## 核心理念

市场比个人更接近真实预期。当不同观点在市场中交汇，价格会反映信息、情绪与判断的综合结果。预测市场并非单纯"猜结果"，而是对未来可能性的定价过程。

## 平台优势

### 概率即价格
每个市场的价格代表当前预测概率，无需赔率换算，市场直接给出实时判断。

### 多领域预测
覆盖：加密货币走势、体育赛事结果、娱乐八卦爆料、全球地缘与气候事件。一个账户，参与全球话题。

### 链上结算，透明可查
所有交易与市场结果公开透明，裁决规则清晰明确。

### 试玩模式友好
新用户可通过试玩模式熟悉预测机制，在理解规则后再参与真实市场。

### 用户可创建市场
不仅参与预测，也可发起预测。市场由社区共建，而非平台单向提供。

### 实时市场更新
市场会围绕热点持续更新，并提供最新的概率变化参考。

## 预测领域

- 加密货币走势：比特币是否突破新高？以太坊能否创历史纪录？
- 体育赛事结果：世界杯、NBA、欧冠、电竞赛事等关键比赛结果预测
- 娱乐八卦与社会热点：明星动态、票房表现、网络热议事件
- 地缘政治与全球趋势：国际局势变化、政策动向、重大公共事件

## 支持的区块链

当前支持：Solana、Polygon、Ethereum、BSC、Base、BTC L2（部分功能）。更多链上线中。

## 支持的钱包

MetaMask、Phantom、OKX Wallet、Coinbase Wallet 以及所有 WalletConnect 钱包。

## 资金安全

所有用户投注资金均部署并托管在智能合约中，保证资金安全透明、无法被平台随意动用。核心市场逻辑全部链上执行，包括：市场创建、交易、状态变更、最终结算。

## 语言支持

平台支持中文和英文界面。


path: references/seers-trading.md
content: # Seers Markets - 超短期权快速交易

## 交易机制

Seers 提供超短期权快速交易功能，用户可以预测加密货币短期价格走势（看涨/看跌）。

### 交易流程

1. 选择交易对（如 BTC/USDT、ETH/USDT）
2. 选择方向：看涨（买涨）或看跌（买跌）
3. 输入下注金额（最低 0.1 USDT，支持两位小数）
4. 系统根据实时赔率计算潜在收益
5. 确认下单
6. 等待结算（5秒结算周期）

### 胜负判定

- 买涨：最终结算价大于开仓成交价时，用户赢
- 买跌：最终结算价小于开仓成交价时，用户赢
- 赢了获得下注金额乘以赔率的奖励
- 输了则损失下注金额

### 结算时间

- 开仓时间精确到毫秒（例：12:23:45.456）
- 结算时间为整秒结算（例：12:23:48.000）

## 赔率说明

### 动态赔率

赔率会根据全站用户的下注情况实时变动：
- 基础赔率约 1.9 倍
- 最高赔率不超过 2.3 倍
- 最低赔率不低于 1.35 倍

### 赔率变动规则

- 买涨的人越多、资金越大，看涨赔率越低
- 买跌的人越多、资金越大，看跌赔率越低
- 资金少的方向赔率会自动升高，吸引用户对冲
- 赔率通过 WebSocket 实时推送，前端实时显示

## 价格来源

### 三家交易所均价

平台价格不依赖单一交易所，采用三家主流交易所的均价：
- Binance（币安）
- OKX（欧易）
- Gate.io（芝麻开门）

计算方式：平台实时价 = (币安价 + OKX价 + Gate价) / 3

### 价格精度

所有交易对价格统一保留 4 位小数（例如：68500.1234）

### 异常处理

- 3 家正常：三家均价
- 1 家掉线：剩余 2 家均价
- 2 家掉线：使用仅剩的 1 家价格
- 全部中断超过 3 秒：触发熔断，前端显示"数据恢复中"，暂停交易

## 熔断机制

当价格源中断超过 3 秒：
- 系统冻结下单按钮，停止接收新订单
- 在途订单按最后获取到的价格结算
- 如果结算时无法取价，本金 100% 原路退回用户


path: references/seers-user-guide-en.md
content: # Seers User Guide (English)

## What is Seers?

Seers is a multi-chain decentralized prediction market platform that allows users to:

- Create prediction events (pay 1000 USDT as deposit)
- Participate in event trading (buy/sell YES/NO positions)
- Submit evidence for arbitration
- Earn rewards based on event outcomes

Seers uses a pool-based market mechanism for predicting and settling future events. Users place bets to form pools for different options, and rewards are ultimately distributed based on the proportion of bets in the correct option pool.

## Is Seers a Market Maker?

No. Seers is not a market maker.

On Seers, users are the core drivers—

- Users are responsible for creating events
- Users bear the event deposit
- Users can profit from event trading fees

Seers official only handles:

- Arbitration
- Security maintenance
- On-chain mechanism execution

The platform remains strictly neutral, does not bet on or manipulate market outcomes, and only intervenes in rare disputed events requiring secondary rulings to maintain overall fairness and user interests.

## How to Create an Event?

Any user can create an event by:

1. Connecting a supported wallet (e.g., MetaMask / Phantom)
2. Selecting a chain (Solana / Polygon / Ethereum, etc.)
3. Setting up:
   - Event title
   - Description
   - Rules and resolution criteria
   - Expiration time

To meet different needs, we offer two creation methods:
- Traditional Mode: No deposit required, suitable for most regular events.
- Maker Mode: Creators pay a configurable deposit to incentivize higher quality events and maintain platform ecosystem.

## Why Pay a Deposit?

The deposit is used for:

- Preventing malicious event creation
- Providing initial market credibility
- Compensating for disputed or voided events
- Ensuring participants have real cost involvement

The deposit is automatically refunded after final settlement (unless the event violates rules or incurs dispute costs).

## How to Trade?

In any event, you can choose:

- YES (believe the event will happen)
- NO (believe the event won't happen)

Buy shares of the corresponding option and join the pool. The market shows the current fund ratio of each option, and at settlement, winners receive rewards based on their share in the correct pool.

When the event ends:

- Correct prediction → Receive payout
- Wrong prediction → Position goes to zero

## Who Decides the Outcome?

Seers uses a hybrid arbitration mechanism:

### 1. User Evidence Submission

Anyone can upload:

- On-chain data screenshots
- Official announcements
- News media
- Sports results
- Trusted third-party information

### 2. Official Arbitration (Final Arbitration)

The Seers arbitration team publishes the final ruling on-chain.

This helps avoid:

- Malicious event creators
- Third-party manipulation
- Data falsification

## Can Seers Manipulate Results?

No.

Seers' responsibilities are:

- Executing on-chain rules
- Ensuring fair and transparent mechanisms
- Providing final arbitration

Seers does not participate in market trading or bet on events, only maintaining platform operations through reasonable fees.

## Which Chains are Supported?

Currently supported:

- Solana
- Polygon
- Ethereum
- BSC
- Base
- BTC (partial features)

More chains coming soon.

## What Events Can Be Created?

Any event with clear, verifiable outcomes, such as:

- Crypto markets (Will BTC break 100k)
- Sports events
- Tech launches and product events
- Policy or social trends
- Real-time hot topics
- Crypto ecosystem data (Gas, TVL, active users, etc.)

Prohibited:

- Content involving personal privacy
- High-risk political inflammatory content
- Content that cannot be objectively verified

Seers reserves the right to reject or terminate violating events in extreme cases.


path: references/seers-user-guide-zh.md
content: # Seers 用户指南 (中文)

## Seers是什么?

Seers 是一个多链去中心化预测市场平台，允许用户：

- 创建预测事件（支付 1000 USDT 作为保证金）
- 参与事件交易（买卖 YES/NO 头寸）
- 提交裁定证据
- 基于事件结果获得奖励

Seers 利用资金池式的市场机制对未来事件进行预测和结算，用户投注形成不同选项的资金池，最终按照投注在正确选项资金池中的占比分配奖励。

## Seers 是平台庄家吗?

不是。Seers 不是庄家。

在 Seers 上，用户才是核心驱动者——

- 用户负责创建事件
- 用户承担事件的保证金
- 用户可以从事件的交易手续费中获利

Seers 官方只承担：

- 仲裁
- 安全维护
- 链上机制执行

平台保持严格中立，不会对市场结果进行投注或操控，仅在极少数争议事件需要二次裁决时介入，以维护整体公平和用户利益。

## 事件如何创建?

任何用户都可以创建事件，只需：

1. 连接支持的钱包（如 MetaMask / Phantom）
2. 选择链（Solana / Polygon / Ethereum 等）
3. 设置：
   - 事件标题
   - 描述
   - 规则与裁定标准
   - 到期时间

为满足不同需求，我们提供两种创建方式：
- 传统模式：无需支付保证金，适合大多数普通事件创建。
- 庄家模式：创建者需支付一笔可配置的保证金，以激励提供更高质量的事件并维护平台生态。

## 为什么要交保证金?

保证金用于：

- 防止恶意事件创建
- 提供初始市场信誉
- 用于争议奖励或作废事件时的补偿
- 确保参与者对事件有真实成本参与

保证金会在事件最终结算后自动退还（除非事件违规或出现争议成本）。

## 如何交易?

在任意事件中，你可以选择：

- YES（认为事件会发生）
- NO（认为事件不会发生）

购买对应选项的份额并加入资金池。市场会显示当前各选项的资金占比，最终结算时，赢家按照自己在正确资金池中的份额占比获得奖励。

事件结束时：

- 预测正确 → 获得 payout
- 预测错误 → 头寸归零

## 谁决定事件结果?

Seers 采用混合仲裁机制：

### 1. 用户证据提交（Evidence Submission）

任何人都可以上传：

- 链上数据截图
- 官方公告
- 新闻媒体
- 赛事结果
- 可信第三方信息

### 2. 官方仲裁（Final Arbitration）

Seers 仲裁团队在链上公布最终裁定。

这是为了避免：

- 恶意事件创建者
- 无关第三方操纵
- 数据伪造

## Seers 能操控事件结果吗?

不能。

Seers 的职责是：

- 执行链上规则
- 确保障机制公平透明
- 提供最终仲裁

Seers 不参与市场交易、不对事件下注，仅通过收取合理的手续费（抽水）维持平台运营和可持续发展。

## 支持哪些链?

当前支持：

- Solana
- Polygon
- Ethereum
- BSC
- Base
- BTC（部分功能）

更多链会持续上线。

## 可以创建哪些事件?

只要事件有明确、可验证的结果即可，例如：

- 加密市场（BTC 是否突破 100k）
- 体育赛事
- 科技发布与产品事件
- 政策或社会趋势
- 实时热点话题
- 加密生态数据（Gas、TVL、活跃用户等）

禁止：

- 涉及真人隐私
- 高风险政治煽动性内容
- 无法客观验证的内容

Seers 官方有权在极端情况下拒绝或终止违规事件。

## 什么是 Seers?

Seers 是一个多链去中心化预测市场平台，允许用户创建事件、交易未来结果，并根据事件最终裁定获得收益。

平台采用公平的资金池机制：用户将资金投入YES或NO选项对应的资金池中，所有资金由智能合约托管和管理。事件结束后，由平台进行公正裁定，正确选项的资金池（扣除手续费后）将按用户投注占比分配给所有赢家。


path: scripts/agent.py
content: """Claude agent: RAG-based Q&A with handoff detection."""
import anthropic
from config import Config
from knowledge import KnowledgeBase


class Agent:
    def __init__(self, config: Config, kb: KnowledgeBase):
        self.config = config
        self.kb = kb
        self.client = anthropic.Anthropic(
            [REDACTED_SECRET],
            base_url=config.anthropic_base_url,
        )
        # Per-user conversation history: {chat_id: [messages]}
        self.histories: dict[str, list[dict]] = {}

    def chat(self, chat_id: str, user_message: str) -> dict:
        """Process user message, return {"reply": str, "handoff": bool}."""
        # Search knowledge base
        context_hits = self.kb.search(user_message, top_k=3)
        context_text = self._format_context(context_hits)

        # Build messages
        history = self.histories.get(chat_id, [])
        history.append({"role": "user", "content": user_message})

        # Trim history
        if len(history) > self.config.max_history:
            history = history[-self.config.max_history:]

        # Build system prompt with context
        system = self.config.system_prompt
        if context_text:
            system += f"\n\n--- Knowledge Base Context ---\n{context_text}\n--- End Context ---"

        # Call Claude
        response = self.client.messages.create(
            model=self.config.model,
            max_tokens=1024,
            system=system,
            messages=history,
        )

        reply = response.content[0].text

        # Check handoff
        handoff = "[HANDOFF]" in reply
        if handoff:
            reply = reply.replace("[HANDOFF]", "").strip()

        # Save history
        history.append({"role": "assistant", "content": reply})
        self.histories[chat_id] = history

        return {"reply": reply, "handoff": handoff}

    def clear_history(self, chat_id: str):
        """Clear conversation history for a user."""
        self.histories.pop(chat_id, None)

    def _format_context(self, hits: list[dict]) -> str:
        if not hits:
            return ""
        parts = []
        for h in hits:
            parts.append(f"[Source: {h['section']}]\n{h['text']}")
        return "\n\n".join(parts)


path: scripts/config.py
content: import os
from pathlib import Path
from dataclasses import dataclass, field
from dotenv import load_dotenv

load_dotenv()


@dataclass
class Config:
    # Claude
    anthropic_api_key: str = os.getenv("ANTHROPIC_API_KEY", "")
    anthropic_base_url: str = os.getenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
    model: str = os.getenv("MODEL", "claude-sonnet-4-20250514")

    # Telegram
    telegram_api_id: str = os.getenv("TELEGRAM_API_ID", "")
    telegram_api_hash: str = os.getenv("TELEGRAM_API_HASH", "")
    telegram_profile: str = os.getenv("TELEGRAM_PROFILE", "default")

    # Agent
    max_history: int = int(os.getenv("MAX_HISTORY", "20"))
    confidence_threshold: float = float(os.getenv("CONFIDENCE_THRESHOLD", "0.6"))
    knowledge_dir: str = os.getenv("KNOWLEDGE_DIR", "docs")

    # Handoff
    handoff_chat_id: str = os.getenv("HANDOFF_CHAT_ID", "")

    # System prompt
    system_prompt: str = field(default="")

    @property
    def session_path(self) -> Path:
        """Telethon session file path."""
        session_dir = Path.home() / ".tgctl-telethon" / self.telegram_profile
        session_dir.mkdir(parents=True, exist_ok=True)
        return session_dir / "session"

    def __post_init__(self):
        self.system_prompt = """You are a professional customer service agent for Seers Markets (https://seers.market), a decentralized prediction market platform.

Rules:
- Answer questions based ONLY on the provided knowledge base context
- ALWAYS reply in the same language the user is using. If the user writes in Chinese, reply in Chinese. If in English, reply in English. Never mix languages unless the user does.
- The knowledge base is written in Chinese. Even when the user asks in English, the context provided will be in Chinese — translate the relevant information into the user's language when answering.
- Be friendly, professional, and concise
- Never make up information about prices, returns, or financial advice
- When explaining trading mechanics, be clear about risks involved
- If the user has typos, try your best to understand their intent and answer accordingly
- If you can partially answer a question, do so — only escalate when you truly cannot help at all

Handoff rules (IMPORTANT — be conservative):
- ONLY add [HANDOFF] at the end of your message when ALL of these are true:
  1. The question is about account-specific issues (login problems, fund stuck, withdrawal failures)
  2. OR you genuinely have zero relevant information to answer the question
  3. OR the user explicitly asks for human support
- Do NOT add [HANDOFF] just because you're slightly unsure — give your best answer instead
- Do NOT add [HANDOFF] for general questions about the platform, trading, or features
"""


path: scripts/knowledge.py
content: """Knowledge base: load Markdown docs, vectorize, and search."""
import os
import glob
import logging
import chromadb
from chromadb.utils import embedding_functions

# Suppress model loading warnings
logging.getLogger("sentence_transformers").setLevel(logging.ERROR)


class KnowledgeBase:
    def __init__(self, docs_dir: str):
        self.docs_dir = docs_dir
        self.ef = embedding_functions.SentenceTransformerEmbeddingFunction(
            model_name=[REDACTED_SECRET]
        )
        self.client = chromadb.Client()
        self.collection = self.client.get_or_create_collection(
            name="knowledge", embedding_function=self.ef
        )
        self._loaded = False

    def load(self):
        """Load all markdown files from docs_dir into vector store."""
        if self._loaded and self.collection.count() > 0:
            return

        md_files = glob.glob(os.path.join(self.docs_dir, "**/*.md"), recursive=True)
        if not md_files:
            print(f"[KB] No markdown files found in {self.docs_dir}")
            return

        ids, documents, metadatas = [], [], []
        for filepath in md_files:
            with open(filepath, "r", encoding="utf-8") as f:
                content = f.read()

            # Split by sections (## headers)
            chunks = self._split_by_sections(content, filepath)
            for i, chunk in enumerate(chunks):
                doc_id = f"{os.path.basename(filepath)}_{i}"
                ids.append(doc_id)
                documents.append(chunk["text"])
                metadatas.append({"source": filepath, "section": chunk["section"]})

        if ids:
            self.collection.upsert(ids=ids, documents=documents, metadatas=metadatas)
            print(f"[KB] Loaded {len(ids)} chunks from {len(md_files)} files")

        self._loaded = True

    def search(self, query: str, top_k: int = 3) -> list[dict]:
        """Search knowledge base, return top_k relevant chunks."""
        if self.collection.count() == 0:
            return []

        results = self.collection.query(query_texts=[query], n_results=top_k)

        hits = []
        for i in range(len(results["ids"][0])):
            hits.append({
                "text": results["documents"][0][i],
                "source": results["metadatas"][0][i]["source"],
                "section": results["metadatas"][0][i]["section"],
                "distance": results["distances"][0][i],
            })
        return hits

    def _split_by_sections(self, content: str, filepath: str) -> list[dict]:
        """Split markdown content by ## headers into chunks."""
        lines = content.split("\n")
        chunks = []
        current_section = os.path.basename(filepath)
        current_lines = []

        for line in lines:
            if line.startswith("## "):
                if current_lines:
                    text = "\n".join(current_lines).strip()
                    if text:
                        chunks.append({"section": current_section, "text": text})
                current_section = line.lstrip("# ").strip()
                current_lines = [line]
            else:
                current_lines.append(line)

        if current_lines:
            text = "\n".join(current_lines).strip()
            if text:
                chunks.append({"section": current_section, "text": text})

        # If no sections found, return whole file as one chunk
        if not chunks:
            chunks.append({"section": os.path.basename(filepath), "text": content.strip()})

        return chunks


path: scripts/main.py
content: """Main entry point: wire everything together and run."""
import sys
import asyncio
from config import Config
from knowledge import KnowledgeBase
from agent import Agent
from telegram_client import TelegramClient


async def main():
    config = Config()

    # Validate config
    if not config.anthropic_api_key:
        print("Error: ANTHROPIC_API_KEY not set")
        sys.exit(1)
    if not config.telegram_api_id or not config.telegram_api_hash:
        print("Error: TELEGRAM_API_ID / TELEGRAM_API_HASH not set")
        sys.exit(1)

    # Init components
    print("[INIT] Loading knowledge base...")
    kb = KnowledgeBase(config.knowledge_dir)
    kb.load()

    print("[INIT] Initializing agent...")
    agent = Agent(config, kb)

    print("[INIT] Connecting to Telegram...")
    tg = TelegramClient(config)
    await tg.connect()

    me = await tg.get_me()
    print(f"[INIT] Logged in as: {me.get('name', 'unknown')} ({me.get('id', '')})")

    async def on_message(chat_id: str, sender_id: str, text: str):
        # Skip own messages
        if sender_id == str(tg.my_id):
            return

        # Commands
        if text.strip().lower() == "/clear":
            agent.clear_history(chat_id)
            await tg.send(chat_id, "✅ Conversation history cleared.")
            return

        if text.strip().lower() == "/help":
            await tg.send(chat_id, (
                "🤖 Customer Service Bot\n\n"
                "Ask me anything about our project.\n"
                "/clear - Clear conversation history\n"
                "/human - Request human support\n"
                "/help - Show this message"
            ))
            return

        if text.strip().lower() == "/human":
            await _handoff(tg, config, chat_id, sender_id, text="User requested human support")
            await tg.send(chat_id, "🙋 已通知人工客服，稍后会有专人联系您。")
            return

        # Agent response
        print(f"[MSG] {sender_id} in {chat_id}: {text[:80]}")
        try:
            await tg.set_typing(chat_id, True)
            result = agent.chat(chat_id, text)
            await tg.set_typing(chat_id, False)
            await tg.send(chat_id, result["reply"])

            if result["handoff"]:
                await _handoff(tg, config, chat_id, sender_id, text)
                await tg.send(chat_id, "🙋 我已通知人工客服，稍后会有专人为您服务。")

        except Exception as e:
            print(f"[ERROR] {e}")
            await tg.send(chat_id, "⚠️ Sorry, something went wrong. Please try again or type /human for support.")

    print("[READY] Listening for messages...")
    await tg.listen(on_message)


async def _handoff(tg: TelegramClient, config: Config, chat_id: str, sender_id: str, text: str):
    """Notify human agent about escalation."""
    if not config.handoff_chat_id:
        print("[HANDOFF] No handoff_chat_id configured, skipping")
        return

    notice = (
        f"🚨 Human support requested\n"
        f"Chat: {chat_id}\n"
        f"User: {sender_id}\n"
        f"Message: {text[:200]}"
    )
    await tg.send(config.handoff_chat_id, notice)
    print(f"[HANDOFF] Notified {config.handoff_chat_id}")


if __name__ == "__main__":
    asyncio.run(main())


path: scripts/requirements.txt
content: anthropic>=0.42.0
chromadb>=0.5.0
sentence-transformers>=3.0.0
python-dotenv>=1.0.0
telethon>=1.37.0


path: scripts/telegram_client.py
content: """Telegram interface: Telethon-based, replaces subprocess tgctl calls."""
import asyncio
from telethon import TelegramClient as TelethonClient, events
from telethon.tl.functions.messages import SetTypingRequest
from telethon.tl.types import User, SendMessageTypingAction, SendMessageCancelAction
from config import Config


class TelegramClient:
    def __init__(self, config: Config):
        self.config = config
        self.client = TelethonClient(
            str(config.session_path),
            int(config.telegram_api_id),
            config.telegram_api_hash,
        )
        self._me = None

    async def connect(self):
        """Connect and verify auth. Auto-login if no session exists."""
        await self.client.connect()
        if not await self.client.is_user_authorized():
            print("[TG] No session found. Starting interactive login...")
            phone = input("Enter your phone number (e.g. +8613800138000): ").strip()
            await self.client.send_code_request(phone)
            code = input("Enter the verification code from Telegram: ").strip()
            try:
                await self.client.sign_in(phone, code)
            except Exception:
                [REDACTED_SECRET]"2FA password required: ").strip()
                await self.client.sign_in([REDACTED_SECRET])
            print("[TG] Login successful!")
        self._me = await self.client.get_me()
        print(f"[TG] Connected as {self._me.first_name} (@{self._me.username}, ID: {self._me.id})")

    async def disconnect(self):
        await self.client.disconnect()

    @property
    def my_id(self) -> int:
        return self._me.id if self._me else 0

    async def set_typing(self, chat_id, typing: bool = True):
        """Set or cancel typing status."""
        try:
            target = int(chat_id) if str(chat_id).lstrip("-").isdigit() else chat_id
            entity = await self.client.get_input_entity(target)
            if typing:
                await self.client(SetTypingRequest(peer=entity, action=SendMessageTypingAction()))
            else:
                await self.client(SetTypingRequest(peer=entity, action=SendMessageCancelAction()))
        except Exception as e:
            print(f"[TG] Failed to set typing for {chat_id}: {e}")

    async def send(self, chat_id, message: str) -> bool:
        """Send a message."""
        try:
            target = int(chat_id) if str(chat_id).lstrip("-").isdigit() else chat_id
            await self.client.send_message(target, message)
            print(f"[TG] Sent to {chat_id}: {message[:60]}")
            return True
        except Exception as e:
            print(f"[TG] Failed to send to {chat_id}: {e}")
            return False

    async def get_me(self) -> dict:
        """Get current user info."""
        if not self._me:
            return {}
        name = " ".join(filter(None, [self._me.first_name, self._me.last_name]))
        return {
            "id": str(self._me.id),
            "name": name,
            "phone": self._me.phone or "",
            "username": self._me.username or "",
        }

    async def listen(self, callback):
        """Listen for incoming messages, call callback(chat_id, sender_id, text) for each."""
        print("[TG] Starting message listener...")

        @self.client.on(events.NewMessage(incoming=True))
        async def handler(event):
            chat_id = str(event.chat_id)
            sender_id = str(event.sender_id)
            text = event.raw_text or ""
            if text:
                await callback(chat_id, sender_id, text)

        await self.client.run_until_disconnected()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)