Skip to content

deese/pycl

Repository files navigation

pycl

IRC chat log forensic analyzer for moderation disputes. Extracts a time window from a WeeChat log, traces the participants, and feeds the transcript to an LLM for analysis — with an interactive TUI for review.


Features

  • Binary search over large log files (O(log n)) — no full scan needed
  • Nick-change tracking: follows a user through alias changes within the window
  • Participant expansion: auto-adds nicks that were directly mentioned or replied to; suggests indirect participants
  • Sentiment analysis: lightweight lexicon (default) or transformer-based (-S); optional second-pass LLM context via --inject-ratings
  • Interactive TUI: colorized transcript, nick visibility toggle, filter/search, LLM chat panel, runtime prompt selector
  • LLM integration: one-shot forensic analysis or iterative chat via OpenRouter (streaming)
  • Custom system prompt: bring your own prompt file to tailor the LLM role; append channel rules with --rules
  • Token/cost estimate: shown before sending anything to the LLM
  • Export: HTML or Markdown transcript dump

Requirements

  • Python 3.11+
  • pyrg (local dependency — ripgrep wrapper)
  • An OpenRouter API key for LLM features

Installation

# From the repo root
pip install -e .

# With transformer-based sentiment analysis
pip install -e ".[sentiment]"

Configuration

Copy .env.example to .env in your working directory (or ~/.config/pycl/.env):

cp .env.example .env
# Required for LLM analysis
OPENROUTER_API_KEY=sk-or-...

# Model to use (default: anthropic/claude-opus-4-8)
PYCL_MODEL=anthropic/claude-opus-4-8

# Initial chat panel height in the TUI (6–50, default: 13)
PYCL_CHAT_HEIGHT=13

# Default system prompt file (overridden by --prompt on the CLI)
# PYCL_PROMPT_FILE=/path/to/prompt.md

# Default channel rules file (overridden by --rules on the CLI)
# PYCL_RULES_FILE=/path/to/rules.md

Usage

Basic — trace specific participants

pycl analyze #madrid.04.2025.log \
  --nicks alice,bob \
  --from "day 13 at 18:00" \
  --to   "day 13 at 19:00"

Open window — no nick filter

pycl analyze #madrid.04.2025.log \
  --from "2025-04-13 18:00:00" \
  --period "+1h"

Without --nicks the full window is loaded and sent to the LLM. A warning is printed when the window exceeds 500 lines.

Static output (no TUI)

pycl analyze #madrid.04.2025.log \
  --nicks alice,bob \
  --from "day 13 at 18" --period "+2h" \
  --no-tui

After the pager closes, a token count and cost estimate for the LLM request are printed.

Export transcript

pycl analyze #madrid.04.2025.log \
  --nicks alice,bob \
  --from "day 13 at 18" --to "day 13 at 19" \
  --export html

Custom system prompt

pycl analyze #madrid.04.2025.log \
  --nicks alice,bob \
  --from "day 13 at 18" --to "day 13 at 19" \
  --prompt rules/IG\ rules.md

All flags

Flag Default Description
--nicks NICKS (none) Comma-separated nicks to trace. Omit for full window.
--from EXPR (required) Window start — ISO datetime or natural language ("day 13 at 18")
--to EXPR Window end (mutually exclusive with --period)
--period DELTA Duration from --from (e.g. +1h, +30m)
--model MODEL PYCL_MODEL / built-in OpenRouter model ID
--prompt FILE PYCL_PROMPT_FILE / built-in System prompt file (.md)
--rules FILE PYCL_RULES_FILE Channel rules file (.md) — appended after the system prompt
-S, --sentiment off Use pysentimiento transformer (requires pycl[sentiment])
--inject-ratings off Prepend per-user sentiment scores to the LLM system prompt (second-pass context)
--participant-window SECS 60 Seconds window for indirect participant suggestions
--no-tui off Static Rich output + pager instead of the TUI
--export {html,md} Export transcript to file
--no-cache off Force recalculation (ignore all caches)
--debug off Print internal debug info to stderr

TUI keybindings

Key Action
f Open Filter bar — hides non-matching lines
/ Open Search bar — highlights matches, all lines visible
n / Shift+N Next / previous search match
a Add nick to trace
p Open Prompt selector — switch system prompt at runtime
Ctrl+L Run forensic LLM analysis (one-shot, streaming)
e Export transcript to HTML
+ / - Grow / shrink chat panel
q Quit
Esc Close filter/search bar

Within the chat panel, type a message and press Enter to chat with the LLM using the transcript as context.


Log format

WeeChat tab-separated format:

YYYY-MM-DD HH:MM:SS\t<nick>\t<message>
YYYY-MM-DD HH:MM:SS\t-!-\t<event body>

Year/month are inferred from the filename (#channel.04.2025.log) for natural-language time expressions.

See docs/LOG_FORMAT.md for the complete format specification and LLM interpretation guide.


Architecture

src/pycl/
├── cli.py          Entry point, argument parsing, output routing
├── window.py       Binary search (mmap) for time-window extraction
├── readers/        Log format parsers
│   ├── base.py     BaseReader protocol
│   └── weechat.py  WeeChat tab-separated format parser
├── filtering.py    Nick-change expansion, participant detection (via pyrg)
├── identity.py     Identity dataclass helpers and color assignment
├── cache.py        3-layer cache: window slice / filtered events / LLM response
├── render.py       Rich 256-color transcript rendering
├── sentiment.py    Lexicon and transformer sentiment backends
├── llm.py          OpenRouter client (streaming), token/cost estimation
├── config.py       .env loading, typed env getters
├── defaults.py     Constants (nick palette, heights, prompt version)
└── tui/
    ├── app.py          PyclApp — main Textual application, prompt selector modal
    ├── transcript.py   Scrollable transcript with filter/search modes
    ├── sidebar.py      Identity list and participant suggestions
    ├── chat.py         LLM chat panel with streaming and busy indicator
    └── styles.tcss     Layout and theming

Cache files are stored in .cache/ next to the log file. Each entry has a metadata sidecar (mtime + size) for automatic invalidation when the source file changes.


Custom prompts and channel rules

--prompt FILE — replace the system prompt

Replaces the built-in forensic framework entirely with the contents of a Markdown file:

pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
  --prompt my_prompt.md

--rules FILE — append channel rules

Appends the rules file after the forensic framework (or custom prompt) so the LLM keeps the analysis structure and can also cite specific rule violations:

pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
  --rules "rules/IG rules.md"

The LLM receives:

[forensic analyst role — built-in or custom]

---

## Channel rules in effect

[contents of rules file]

Included prompt variants

File Use case
prompts/moderator_audit.md Evaluate whether the operators acted within the rules — kick reasons, ban compliance, mode limits, akick renewal
prompts/incident_report.md Structured formal report — participants, timeline, rule violations, severity 1-5, recommended action — suitable for submitting to network admins
prompts/quick_triage.md Fast decision: single paragraph + classification (IGNORE / WARN / KICK / BAN / ESCALATE) + key evidence — no deep analysis

The built-in prompt (SYSTEM_PROMPT_claude.md) covers deep forensic analysis of user-vs-user disputes.

Combining prompts with rules

Both flags can be combined to use a custom base prompt with specific rules appended:

pycl analyze log.txt --nicks alice --from "18:00" --to "19:00" \
  --prompt prompts/senior_mod.md \
  --rules  "rules/IG rules.md"

Set defaults in .env to avoid specifying them on every invocation:

PYCL_PROMPT_FILE=prompts/senior_mod.md
PYCL_RULES_FILE=rules/IG rules.md

Sentiment analysis

The --sentiment / -S flag activates the pysentimiento transformer backend (Spanish, POS/NEG/NEU + emotion). Without it, a fast lexicon-based backend runs by default.

Use --inject-ratings to prepend per-user aggregate scores to the LLM system prompt as a second-pass context block:

pycl analyze log.txt --nicks alice,bob --from "18:00" --to "19:00" \
  -S --inject-ratings

This is useful when sentiment scores already give a clear signal and you want the LLM to interpret them as part of the forensic analysis.


Environment variables reference

Variable Description
OPENROUTER_API_KEY API key (required for LLM)
PYCL_MODEL Default OpenRouter model ID
PYCL_CHAT_HEIGHT Initial TUI chat panel height (6–50)
PYCL_PROMPT_FILE Default system prompt file path
PYCL_RULES_FILE Default channel rules file path

TODO

  • Run the LLM analysis only with the nicks with negative sentiment.

About

python chat colourer

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages