feat: replace faqqer with multi-channel AI help bot (AnythingLLM + Discord + Telegram)#3
feat: replace faqqer with multi-channel AI help bot (AnythingLLM + Discord + Telegram)#30xPepeSilvia wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request upgrades the Tari AI Help Bot to version 2, implementing a Retrieval-Augmented Generation (RAG) pipeline powered by AnythingLLM. The new architecture features a multi-channel bridge worker supporting Telegram and Discord, automated knowledge base seeding from local files, and a feedback system for admin-approved learning. It also integrates legacy scheduled jobs using APScheduler. Review feedback identifies several improvement opportunities: optimizing performance by moving the HTTP client instantiation outside of retry loops, enhancing Discord bot reliability by utilizing member data directly from event payloads, and preventing potential API errors in the Telegram bot by implementing message length truncation.
| async for attempt in AsyncRetrying( | ||
| stop=stop_after_attempt(3), | ||
| wait=wait_exponential(min=1, max=8), | ||
| retry=retry_if_exception_type((httpx.HTTPError,)), | ||
| reraise=True, | ||
| ): | ||
| with attempt: | ||
| async with httpx.AsyncClient( | ||
| timeout=self._timeout, headers=self._headers | ||
| ) as client: | ||
| resp = await client.post(url, json=payload) | ||
| if resp.status_code >= 500: | ||
| raise httpx.HTTPError(f"upstream {resp.status_code}") | ||
| if resp.status_code >= 400: | ||
| raise RAGError( | ||
| f"AnythingLLM rejected the request: " | ||
| f"{resp.status_code} {resp.text[:200]}" | ||
| ) | ||
| data = resp.json() |
There was a problem hiding this comment.
The httpx.AsyncClient is being instantiated inside the retry loop. This is inefficient as it creates a new client and connection pool for every retry attempt. It should be moved outside the AsyncRetrying block to properly leverage connection pooling across retries.
References
- Reuse client sessions to benefit from connection pooling and improve performance.
There was a problem hiding this comment.
Fixed in 99fab49: httpx.AsyncClient is now instantiated once before the AsyncRetrying loop and reused across retry attempts.
| guild = bot.get_guild(payload.guild_id) if payload.guild_id else None | ||
| member = guild.get_member(payload.user_id) if guild else None | ||
| if not self._is_admin(member): | ||
| return |
There was a problem hiding this comment.
In on_raw_reaction_add, the payload object already contains the member object for guild events. Using payload.member is more reliable than guild.get_member, especially since the members intent is not enabled, which would likely result in cache misses for users not already known to the bot.
References
- Use available event payload data to avoid unnecessary API calls or cache lookups.
There was a problem hiding this comment.
Fixed in 99fab49: on_raw_reaction_add now reads payload.member first (populated directly by the Discord gateway event) and only falls back to guild.get_member() when it's None.
| answer = await self._answer(question) | ||
| sent = await msg.reply_text(answer) |
There was a problem hiding this comment.
Telegram has a message length limit of 4096 characters. If the AI response exceeds this limit, the bot will fail to send the reply. Truncation should be applied to ensure the message is within Telegram's limits, similar to the implementation in the Discord bot.
References
- Handle platform-specific message length limits to prevent API errors.
There was a problem hiding this comment.
Fixed in 99fab49: added a _truncate(text, limit=4096) helper; _handle_question now sends _truncate(answer) to stay within Telegram's 4096-character hard limit.
- rag_client: move httpx.AsyncClient instantiation outside the tenacity retry loop so the connection pool is reused across attempts rather than torn down and rebuilt on every retry - discord_bot: prefer payload.member (populated directly by the gateway event) over a cache lookup in on_raw_reaction_add; fall back to guild.get_member() only when payload.member is None - telegram_bot: add _truncate() helper (4096-char Telegram API limit) and apply it in _handle_question() to prevent MessageTooLong errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All three points from the gemini review have been addressed in 1. HTTP client instantiation outside retry loop ( 2. Discord member from event payload ( 3. Telegram message length truncation ( |
Closes #1
Summary
This PR replaces the single-file
faqqer_bot.py(direct OpenAI completions) with a production-ready multi-channel AI help bot backed by AnythingLLM's RAG pipeline. Telegram and Discord Q&A bots are deployed alongside the existing blockchain stats and customer-analysis scheduled jobs, all orchestrated via Docker Compose.Acceptance criteria
docs/ARCHITECTURE.mdbridge/telegram_bot.py→bridge/rag_client.pybridge/discord_bot.py→bridge/rag_client.pybridge/init_kb.py, volume mount indocker-compose.ymlbridge/feedback.py+promote_to_kb()inrag_client.pybridge/jobs.pywraps both viaimportlib(no edits to originals)docker-compose.yml+bridge/DockerfileFramework rationale — why AnythingLLM
:3001without touching code/api/v1/workspace/{slug}/chatis versioned and thin — the whole RAG client is ~194 linesmintplexlabs/anythingllmimage; LanceDB vector store is built-in, no extra infrafaqs/dir and restart bridge; manifest prevents duplicatesArchitecture
Quick deploy
Test results
Test 1 — syntax + import:
Test 2 — docker compose config:
Docker not available in the CI environment; compose YAML was manually validated: correct version, all service keys present, volumes/networks defined, env_file + environment blocks syntactically valid, no port conflicts.
Test 3 — adversarial review + fixes applied:
ANYTHINGLLM_API_KEYmissingconfig.pyraisesRuntimeErrorwith clear message;main.pycatches it, logs and exits with code 2rag_client.wait_until_ready()polls with exponential back-off up to 5 min;main.pyexits with code 3 if never reachableanswerpassed to.reply()/.followup.send()_truncate()helper indiscord_bot.pyapplied at all send sitesfeedback.get()returnsNone; all approve handlers check forNoneand reply gracefully_record_pairinon_messagepathrequirements.txtSQLAlchemy(required byAPScheduler.SQLAlchemyJobStore) andopenai(required bycustomer_analysis_job) were absentSQLAlchemy==2.0.36andopenai==1.54.4🤖 Generated with Claude Code
Bounty payout
If this PR is accepted as a bounty submission, please direct funds to the following Tari wallet address:
123JbAxCZ4Vrh4jCjFLXTu4z8sLeggUR87fejwNunsB55NMBSH9RpkjNiw1WL2GkKr8JhqRZhbrsSREchGanchw2Nhb