DEV Community

miwaty
miwaty

Posted on

I Built a Fully Local OSINT Agent with Ollama, LangChain, Telegram and Qwen3.5 14B — Running 24/7 on My Homelab, Zero Cloud, Zero Compromises

I Built a Fully Local OSINT Agent with Ollama, LangChain, Telegram and Qwen3.5 14B — Running 24/7 on My Homelab, Zero Cloud, Zero Compromises

Disclaimer — This project was built for educational purposes only, as a hands-on way to learn LangChain, local LLMs and OSINT tooling. Every technique described here should only be used on targets you have explicit written authorisation to analyse. Unauthorised use of OSINT tools may be illegal in your jurisdiction. Always act ethically and responsibly.

I've been wanting to seriously learn LangChain for a while. Not just tutorials — actually build something real that I'd use.

So I built OSINT Marin AI: a fully local OSINT agent running on my homelab, accessible from anywhere via Telegram, powered by Qwen3.5 14B running locally via Ollama, with Tor for IP rotation, SQLite for caching, and Matplotlib for statistical charts sent directly to my phone.

No cloud. No API keys. No data leaving my network. Just Python, open-source tools, and a lot of trial and error.


Why local?

If you work anywhere near cybersecurity, you already know the answer. Sending queries about IP addresses, domains, Instagram profiles or email addresses to a cloud API means that data is leaving your network and hitting someone else's servers. In a professional context that's often a hard no.

Local models solve this completely. Everything stays on your hardware, under your control, with no third-party visibility into what you're analysing.

For this project I used Qwen3.5 14B in Q6_K quantization, served by Ollama. Thinking mode disabled — it adds latency with no real benefit for this use case.


What the agent can actually do

I'll be concrete. These are the Telegram commands that work right now:

Instagram:

  • /analizza_profilo @username — full profile metadata + AI summary
  • /scarica_foto @username — downloads photos, shows a paginated interactive list with buttons to view each photo or its metadata (likes, comments, date, hashtags, caption) directly in Telegram
  • /scarica_reel @username — downloads reels
  • /stories @username — downloads stories (requires auth)
  • /foto_profilo @username — downloads and sends the profile picture
  • /statistiche @username — generates and sends 7 statistical charts as PNG images

Web & OSINT:

  • /analizza_sito https://... — full site analysis with AI summary
  • /whois dominio.com — WHOIS + DNS records + AI interpretation
  • /sherlock username — username search across platforms + AI digital profile
  • /ip 1.2.3.4 — geolocation, ISP, ASN + AI comment
  • /email info@x.com — MX records, provider, domain status + AI analysis
  • /telefono +39... — carrier, country, number type + AI comment
  • /chiedi <domanda> — free natural language question to the LLM

Every command that hits an external service first checks the local SQLite database. If the data is already there, it returns it instantly without making a new request.


The stack

python-telegram-bot 21.6     ← user interface
LangChain 0.3.7              ← agent framework
LangChain-Ollama 0.2.1       ← LLM connector
Ollama                       ← local model server
Qwen3.5 14B (Q6_K)          ← the model (no thinking mode)
Instaloader 4.13.1           ← Instagram scraping
SQLAlchemy 2.0.35 + SQLite   ← local database
Matplotlib 3.9.2             ← statistical charts
Sherlock                     ← username OSINT
python-whois + dnspython     ← WHOIS and DNS
phonenumbers                 ← phone number analysis
stem 1.8.2                   ← Tor integration
Loguru 0.7.2                 ← structured logging
Enter fullscreen mode Exit fullscreen mode

Architecture

You (Telegram)
      ↓
  Telegram Bot (python-telegram-bot)
      ↓
  handlers.py — command routing + auth check
      ↓
  tools/ — instagram, web, osint, stats
      ↓
  SQLite DB — always checked before external requests
      ↓
  External sources (Instagram, ip-api, Sherlock...)
      ↓ (optional)
  Tor proxy — IP rotation for Instagram requests
      ↓
  agent/llm.py — ChatOllama, generates AI summary
      ↓
  Back to Telegram — structured data + AI block
Enter fullscreen mode Exit fullscreen mode

The LLM is the last step, not the first. The tools collect the data, the model synthesises it. If Ollama isn't running, the bot works anyway — the AI block is just skipped silently.


The caching layer — the decision I'm most proud of

Every external request is expensive: rate limits, latency, risk of getting blocked. So before every call to Instagram, ip-api, Sherlock or WHOIS, the agent checks SQLite first.

def analizza_profilo(username: str) -> dict:
    # Check local DB first — never hit Instagram twice for the same target
    profilo_db = get_profile(username)
    if profilo_db:
        logger.info(f"Profile {username} found in local database")
        return {...}  # instant response

    # Only here if not cached
    loader = _get_loader(autenticato=False)
    profile = instaloader.Profile.from_username(loader.context, username)
    dati = { ... }
    save_profile(dati)
    return dati
Enter fullscreen mode Exit fullscreen mode

Five tables: profiles, posts, websites, osint_results, logs. Every result stored with full metadata. Over time the local database becomes genuinely useful — a growing knowledge base of everything you've ever analysed.


Tor integration for IP rotation

Instagram rate-limits aggressively. After a few requests, you start getting 401/403 from their GraphQL API. My solution: route Instagram traffic through Tor and rotate the circuit when blocked.

brew install tor
# Add to torrc:
# ControlPort 9051
brew services start tor
Enter fullscreen mode Exit fullscreen mode

In Python, the utils/tor.py module handles circuit rotation via stem. When Instaloader hits a block, the agent automatically requests a new Tor circuit and retries. Not perfect, but it works well enough for homelab use.


The LLM integration — keeping it simple

I didn't build a complex ReAct agent for this. The pattern is simpler and more reliable:

  1. Tool runs, collects structured data
  2. Data is sent to Telegram as formatted text
  3. Same data is passed to _llm_analizza() which selects the right prompt for that data type
  4. Model generates a natural language summary
  5. Summary sent to Telegram as a separate Analisi AI block
llm = ChatOllama(
    model="qwen3.5:14b",
    base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
    temperature=0.1,      # deterministic — no hallucination games
    num_predict=2048,
)
Enter fullscreen mode Exit fullscreen mode

Temperature 0.1 because I want the model to interpret data, not invent it. The system prompt instructs it to never speculate beyond what the tools returned.


The 7 statistical charts

For Instagram profiles, /statistiche @username generates and sends 7 PNG charts:

  • Monthly posting frequency — bar chart
  • Most active hours — heatmap (day of week × hour)
  • Engagement rate over time — line chart (likes + comments / followers × 100)
  • Like evolution — line chart
  • Top hashtags — horizontal bar chart (top 20)
  • Content type breakdown — pie chart (photo / video / reel)
  • Average caption length by month — bar chart

All generated with Matplotlib in Agg mode (no display needed), saved to /tmp/, sent to Telegram, then deleted. The underlying data comes from SQLite — no additional Instagram requests.


Project structure — designed to be extended

osint-marin-ai/
├── main.py               ← entrypoint
├── agent/
│   └── llm.py            ← ChatOllama + verifica_ollama + analizza_con_llm
├── bot/
│   ├── handlers.py       ← all Telegram command handlers
│   └── keyboards.py      ← inline keyboards
├── tools/
│   ├── instagram.py      ← Instaloader
│   ├── web.py            ← site analysis
│   ├── osint.py          ← Sherlock, IP, email, phone, WHOIS
│   └── stats.py          ← Matplotlib charts
├── db/
│   ├── models.py         ← SQLAlchemy models
│   └── database.py       ← CRUD + always-check-db-first pattern
└── utils/
    ├── logger.py         ← Loguru
    └── tor.py            ← Tor proxy + circuit rotation
Enter fullscreen mode Exit fullscreen mode

Adding a new tool takes four steps:

  1. Create a file in /tools/
  2. Save results via db/database.py
  3. Register the command in bot/handlers.py
  4. Add a prompt entry in _llm_analizza() if you want AI summaries

That's the entire extension contract. No hidden coupling, everything is documented in Italian with docstrings.


What I actually learned building this

LangChain is powerful but you don't always need the full agent loop. For most of my commands, a simple tool → LLM → response pattern is faster, more reliable and easier to debug than a full ReAct agent. I'll use LangGraph for the more complex multi-step workflows I'm planning next.

Local LLMs are genuinely usable now. A year ago this would have been frustratingly slow. Today, with Q6_K quantization and thinking mode disabled, the model responds fast enough for interactive Telegram use. The ecosystem has caught up.

Caching is not optional. Instagram will block you. Sherlock takes 60-120 seconds. Without the SQLite caching layer this would be unusable. Cache everything, always check the database first.

Tor helps, but isn't a silver bullet. Instagram's bot detection is sophisticated. Authenticated requests with a dedicated account are still more reliable than Tor rotation for heavy scraping.


What's next

  • LangGraph for multi-step OSINT workflows (gather → correlate → report)
  • SearXNG for fully local web search integrated into the agent
  • PDF report generation — export a complete OSINT report from Telegram
  • Scheduled monitoring — alert when a tracked profile changes
  • Face recognition on downloaded photos

Final thoughts

I started this to learn LangChain. I ended up with a tool I actually use. That's the best possible outcome from a homelab project built for educational purposes.

If you're in security and haven't explored local LLMs yet — the barrier is lower than you think. Ollama makes model management trivial, LangChain handles the agent plumbing, and python-telegram-bot gives you a mobile interface in 50 lines of code.

The stack is mature. The models are capable. The only thing missing is someone building with them.


Written by miwaty — cybersecurity enthusiast, homelab builder, eternal work in progress.

Top comments (0)