Feat/m4 telegram bot#28
Merged
Merged
Conversation
This was
linked to
issues
May 25, 2026
There was a problem hiding this comment.
Pull request overview
Adds a new “trade-compass” Telegram bot deployed on Cloud Run, with scheduled push notifications, and extends the existing API to support LLM model selection and decision history.
Changes:
- Adds a new
terraform/botstack to build/deploy the bot to Cloud Run and create Cloud Scheduler push jobs, plus deploy script automation to wire env vars and register Telegram webhooks. - Introduces a new
bot/Python service (FastAPI + python-telegram-bot + LangGraph) that calls FMP + OpenRouter and persists decisions to the existing API. - Updates API routing (no trailing slash redirects), adds
/decisionslisting, and extends preferences withllm_model.
Reviewed changes
Copilot reviewed 40 out of 46 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| terraform/deploy.sh | Extends deployment automation to deploy the bot, update bot/.env, and register Telegram webhook. |
| terraform/cloud_run/main.tf | Switches image build to Cloud Build and adds project-level IAM grants + propagation delay. |
| terraform/bootstrap/main.tf | Enables additional GCP APIs (Cloud Build, Cloud Scheduler). |
| terraform/atlas/main.tf | Updates Atlas IP access list rules for Cloud Run egress. |
| terraform/bot/backend.tf | Adds GCS backend config for the bot Terraform state. |
| terraform/bot/variables.tf | Defines bot Terraform inputs (tokens/keys, project/region, tfstate bucket). |
| terraform/bot/main.tf | Creates bot Cloud Run service, Secret Manager secrets, public invoker binding, and Cloud Scheduler push jobs. |
| terraform/bot/outputs.tf | Exposes the bot Cloud Run URL for webhook registration/testing. |
| bot/main.py | Adds FastAPI app entrypoint with /health, /webhook, and /push. |
| bot/Dockerfile | Defines container image build for the bot service. |
| bot/requirements.txt | Adds runtime Python dependencies for the bot service. |
| bot/requirements-dev.txt | Adds pytest tooling dependencies for the bot. |
| bot/pytest.ini | Configures pytest asyncio mode. |
| bot/.dockerignore | Prevents secrets/tests/dev files from being included in the container build context. |
| bot/.env.example | Documents required runtime env vars for local/integration usage. |
| bot/config.json | Adds configurable OpenRouter model list and default selection. |
| bot/config.py | Loads config.json and exposes model helpers/validation. |
| bot/state.py | Defines the shared analysis workflow state model (TypedDicts). |
| bot/graph/workflow.py | Defines LangGraph workflow topology for single-stock and portfolio analysis. |
| bot/tools/llm.py | Adds OpenRouter-backed LLM client helper via LangChain. |
| bot/tools/market_data.py | Adds FMP async client utilities for quote/profile/financials/news/ratings/scores. |
| bot/tools/portfolio_api.py | Adds async REST client for calling the deployed trade-compass API. |
| bot/tools/prompt.py | Builds the structured decision prompt from analysis outputs. |
| bot/tg/bot.py | Implements Telegram webhook router and Application wiring. |
| bot/tg/handlers.py | Implements /decide, /portfolio, /model, /help Telegram command handlers. |
| bot/tg/push.py | Implements /push endpoint triggered by Cloud Scheduler to send Telegram summaries. |
| bot/tests/test_api.py | Adds integration tests for deployed API connectivity/auth and endpoints. |
| bot/tests/test_fmp.py | Adds integration tests for FMP connectivity and response shapes. |
| bot/tests/test_openrouter.py | Adds integration tests for OpenRouter connectivity and structured outputs. |
| bot/tests/test_push.py | Adds integration test for deployed bot /push endpoint. |
| api/src/main.py | Disables redirect slashes to standardize route shapes. |
| api/src/models.py | Adds llm_model to preferences model. |
| api/src/routers/holdings.py | Changes holdings routes to "" to match redirect_slashes=False. |
| api/src/routers/preferences.py | Changes preferences routes to "" to match redirect_slashes=False. |
| api/src/routers/decisions.py | Adds GET "" list endpoint and changes POST route to "". |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+53
to
+57
| set_env() { | ||
| local key="$1" val="$2" | ||
| local tmp | ||
| tmp="$(mktemp)" | ||
| if grep -q "^${key}=" "${ENV_FILE}" 2>/dev/null; then |
Comment on lines
+96
to
+106
| # Wait for bot Cloud Run to be healthy before registering webhook | ||
| echo " Waiting for bot service to be healthy..." | ||
| for i in $(seq 1 12); do | ||
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BOT_URL}/health" 2>/dev/null || echo "000") | ||
| if [ "${STATUS}" = "200" ]; then | ||
| echo " Bot service is healthy (attempt ${i})" | ||
| break | ||
| fi | ||
| echo " Attempt ${i}/12: status=${STATUS}, retrying in 10s..." | ||
| sleep 10 | ||
| done |
Comment on lines
+108
to
+113
| # Token is passed in the URL path (Telegram API requirement), but we avoid | ||
| # echoing the full URL to logs to reduce accidental token exposure. | ||
| WEBHOOK_RESP=$(curl -s \ | ||
| --url "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \ | ||
| --data-urlencode "url=${BOT_URL}/webhook") | ||
| if echo "${WEBHOOK_RESP}" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('ok') else 1)"; then |
Comment on lines
+42
to
+46
| resource "google_project_iam_member" "cloudbuild_storage" { | ||
| project = var.gcp_project_id | ||
| role = "roles/storage.admin" | ||
| member = local.cloudbuild_sa | ||
| } |
Comment on lines
+62
to
+68
| # M0 free tier does not support VPC peering or private endpoints. | ||
| # Real security is the credentials stored in Secret Manager. | ||
| resource "mongodbatlas_project_ip_access_list" "cloud_run" { | ||
| project_id = mongodbatlas_project.trade_compass.id | ||
| cidr_block = "0.0.0.0/0" | ||
| comment = "Cloud Run egress — M0 does not support VPC peering; auth via Secret Manager" | ||
| } |
Comment on lines
+40
to
+53
| @push_router.post("/push") | ||
| async def push(request: Request, body: PushRequest) -> dict: | ||
| """Triggered by Cloud Scheduler. Runs portfolio analysis and sends to Telegram.""" | ||
| if body.type not in _PUSH_TYPES: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail=f"Unknown push type '{body.type}'. Valid: {list(_PUSH_TYPES)}", | ||
| ) | ||
|
|
||
| chat_id = os.environ.get("TELEGRAM_CHAT_ID") | ||
| token = os.environ.get("TELEGRAM_BOT_TOKEN") | ||
| if not chat_id or not token: | ||
| raise HTTPException(status_code=500, detail="TELEGRAM_CHAT_ID or TELEGRAM_BOT_TOKEN not set") | ||
|
|
Comment on lines
+222
to
+264
| # ── Allow unauthenticated (Telegram webhook needs public access) ────────────── | ||
| resource "google_cloud_run_v2_service_iam_member" "public" { | ||
| name = google_cloud_run_v2_service.bot.name | ||
| location = var.region | ||
| role = "roles/run.invoker" | ||
| member = "allUsers" | ||
| } | ||
|
|
||
| # ── Cloud Scheduler: 5 daily push notifications ─────────────────────────────── | ||
| # All times in UTC (ET = UTC-4 in summer, UTC-5 in winter; using UTC-4 / EDT) | ||
| # pre_market 09:25 ET → 13:25 UTC | ||
| # morning 11:00 ET → 15:00 UTC | ||
| # noon 12:30 ET → 16:30 UTC | ||
| # afternoon 14:30 ET → 18:30 UTC | ||
| # post_market 16:05 ET → 20:05 UTC | ||
|
|
||
| locals { | ||
| push_schedules = { | ||
| pre_market = "25 13 * * 1-5" | ||
| morning = "0 15 * * 1-5" | ||
| noon = "30 16 * * 1-5" | ||
| afternoon = "30 18 * * 1-5" | ||
| post_market = "5 20 * * 1-5" | ||
| } | ||
| } | ||
|
|
||
| resource "google_cloud_scheduler_job" "push" { | ||
| for_each = local.push_schedules | ||
|
|
||
| name = "trade-compass-push-${each.key}" | ||
| description = "trade-compass bot ${each.key} push" | ||
| schedule = each.value | ||
| time_zone = "UTC" | ||
| attempt_deadline = "180s" | ||
|
|
||
| http_target { | ||
| http_method = "POST" | ||
| uri = "${google_cloud_run_v2_service.bot.uri}/push" | ||
| body = base64encode(jsonencode({ type = each.key })) | ||
| headers = { | ||
| "Content-Type" = "application/json" | ||
| } | ||
| } |
Comment on lines
+70
to
+76
| @webhook_router.post("/webhook") | ||
| async def webhook(request: Request) -> Response: | ||
| """Receive Telegram update and dispatch to handlers.""" | ||
| data = await request.json() | ||
| update = Update.de_json(data, application.bot) | ||
| await application.process_update(update) | ||
| return Response(status_code=200) |
Comment on lines
+35
to
+38
| def build_application() -> Application: | ||
| token = os.environ.get("TELEGRAM_BOT_TOKEN", "") | ||
| app = Application.builder().token(token).build() | ||
|
|
Comment on lines
+16
to
+24
| @router.get( | ||
| "", | ||
| responses={200: {"content": {"application/json": {"example": [_example]}}}}, | ||
| ) | ||
| async def list_decisions(): | ||
| """Return all decisions, newest first.""" | ||
| db = get_db() | ||
| return await db.decisions.find({}, {"_id": 0}).sort("created_at", -1).to_list(None) | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.