4.0 KiB
Bale File Agent — How It Works
The idea
The interviewer's brief was: type a natural-language request like "grab the devops PDFs from this channel" and have an agent go fetch them — the way a person would, but automated. This workflow implements that as a short pipeline in n8n, using an LLM only for the one step that actually needs language understanding (turning a sentence into a structured filter), and plain code/API calls for everything else.
The pipeline, step by step
Chat message
↓
LLM parses intent → {file_type, keyword}
↓
Fetch messages the Bale bot has seen
↓
Filter messages against the parsed intent
↓
Download each match from Bale
↓
Save to disk
1. Chat Trigger
A live chat box inside the n8n editor. Whatever you type becomes the field chatInput on the item that flows to the next node. This stands in for "a person typing a request" — in a real deployment this could be a Slack bot, a web form, or Bale itself.
2. Parse command (LLM)
A single HTTP call to BazaarLink's OpenAI-compatible endpoint. The system prompt forces the model into a narrow, mechanical job: read the sentence, output nothing but {"file_type": "...", "keyword": "..."}. This is intentionally the only place an LLM is used — everything else is deterministic, which keeps the workflow fast, cheap, and easy to debug.
3. Extract filters
A small Code node that takes the LLM's raw text response, strips any accidental markdown fences, and parses it into real JSON so the rest of the workflow can use .file_type and .keyword as normal fields.
4. Fetch Bale updates
Calls Bale's Bot API (getUpdates) — a long-polling endpoint that returns every message the bot has seen since it was last called. Bale's bot API is essentially a clone of Telegram's, so this behaves the same way: it returns new activity, not full channel history.
5. Filter matches
Another Code node. Loops through the fetched messages, and for each one:
- Checks whether it's a PDF document or a photo (matching
file_type) - Checks whether the caption or filename contains the requested
keyword - If both match, records the file's
file_idand realfile_namefor the next steps
This is where "understanding the request" turns into "acting on the request" — the filtering logic itself needs no AI, just the structured output the LLM already produced.
6. Get file path → Download file
Two calls to Bale's API mirroring Telegram's own two-step file download: getFile resolves a file_id into a temporary file_path, then a second request downloads the actual bytes from that path.
7. Save to disk
Writes the downloaded binary to a folder on the n8n host, using the real original filename (not Bale's internal file token) so the output is something a human would recognize.
Why it's built this way
- LLM only where it's needed. Intent parsing is genuinely a language task; filtering, fetching, and downloading are not. Using an LLM for the whole thing would be slower, more expensive, and harder to debug than necessary.
- Deterministic filtering. The match logic is plain code, so it's predictable and cheap to test — you can reason about exactly why a file did or didn't match.
- Small, inspectable steps. Each node does one clear thing, so a failure (like a bad file path, or an expired long-poll) is easy to isolate — you can see exactly which stage produced bad data by clicking through node outputs in n8n's UI.
What a production version would add
- A persistent index of channel content (log every incoming file to a small database or sheet as it arrives via webhook), so requests aren't limited to whatever
getUpdateshappens to still be holding. - Retry logic on the LLM call for transient network blips.
- A proper credential store in n8n instead of hardcoded tokens/keys.
- Possibly a semantic match (embeddings or a second LLM pass) instead of plain substring matching on captions, so "grab the devops stuff" also catches a file captioned "infrastructure automation" even without the literal word "devops."