Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Developers report completed work via slash commands. The bot also pulls merged/o
- `/report` (or `/rpt`) — Developers report work items via Slack
- `/fetch` — Pull merged and open GitLab MRs and/or GitHub PRs for the current calendar week
- `/generate-report` (or `/gen`) — Generate a team markdown file (or boss `.eml` draft) and upload it to Slack
- `/list` — View this week's items with inline edit/delete actions
- `/list` — View your items for this week with inline edit/delete actions (`/list all` for the team view)
- `/check` — Managers: list missing members with inline nudge buttons
- `/retrospect` — Managers: analyze recent corrections and suggest glossary/guide improvements
- `/stats` — Managers: view classification accuracy dashboard and trends
Expand Down Expand Up @@ -105,9 +105,9 @@ See [docs/agentic-features-overview.md](docs/agentic-features-overview.md) for a
| `/report` | Report a work item |
| `/rpt` | Alias of `/report` |
| `/fetch` | Fetch merged and open GitLab MRs and/or GitHub PRs for this week |
| `/generate-report` | Generate the weekly report (`team`/`boss`, optional `private`) |
| `/generate-report` | Generate the weekly report (`team`/`boss`) or post latest team report (`post`), optional `private` |
| `/gen` | Alias of `/generate-report` |
| `/list` | List this week's work items |
| `/list` | List your work items for this week (`/list all` for the team view) |
| `/check` | List missing members with nudge buttons |
| `/nudge` | Send a test nudge DM (self by default; managers can target one member) |
| `/retrospect` | Analyze corrections and suggest improvements |
Expand Down Expand Up @@ -147,6 +147,8 @@ llm_example_max_chars: 140 # optional: max chars per example snippet
llm_glossary_path: "./llm_glossary.yaml" # optional glossary memory file
llm_critic_enabled: false # optional: enable generator-critic second pass
anthropic_api_key: "sk-ant-..."
openai_api_key: ""
openai_base_url: "https://api.openai.com/v1" # optional: OpenAI-compatible base URL (for example a lab-hosted gpt-oss endpoint)

# Permissions (Slack user IDs)
manager_slack_ids:
Expand Down Expand Up @@ -190,6 +192,8 @@ export GITLAB_GROUP_ID=my-team
export GITLAB_REF_TICKET_LABEL=Jira # Optional: field label used for GitLab MR ticket parsing
export LLM_PROVIDER=anthropic
export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=
export OPENAI_BASE_URL=https://api.openai.com/v1
export LLM_BATCH_SIZE=50
export LLM_CONFIDENCE_THRESHOLD=0.70
export LLM_EXAMPLE_COUNT=20
Expand All @@ -214,9 +218,11 @@ Note: Category/subcategory headings are sourced from the previous report in `rep
| `openai` | `gpt-5-mini` |

Set `llm_model` in YAML or `LLM_MODEL` env var to override.
When `llm_provider=openai`, section classification uses the OpenAI-compatible `responses` API with schema-constrained JSON output.
Set `llm_batch_size` / `LLM_BATCH_SIZE`, `llm_confidence_threshold` / `LLM_CONFIDENCE_THRESHOLD`, and `llm_example_count` / `llm_example_max_chars` to tune throughput, confidence gating, and prompt context size.
Set `llm_glossary_path` / `LLM_GLOSSARY_PATH` to apply glossary memory rules (see `llm_glossary.yaml`).
Set `llm_critic_enabled` / `LLM_CRITIC_ENABLED` to enable a second LLM pass that reviews classifications for errors.
Set `openai_base_url` / `OPENAI_BASE_URL` when `llm_provider=openai` and you want to use an OpenAI-compatible endpoint instead of `api.openai.com` (for example a lab-hosted `gpt-oss-120b` server).
Set `external_http_timeout_seconds` / `EXTERNAL_HTTP_TIMEOUT_SECONDS` to tune timeout limits for GitLab/GitHub/LLM API requests.

Glossary example (`llm_glossary.yaml`):
Expand Down Expand Up @@ -332,6 +338,8 @@ Manager only. Two modes:
/generate-report team # Generate team markdown (.md) and upload to channel (default)
/generate-report boss # Generate boss email draft (.eml) and upload to channel (default)
/generate-report boss private # Send generated boss report to your DM
/generate-report post # Post latest generated team markdown report to the current channel
/generate-report post private # Post latest generated team markdown report to your DM
/gen private # Generate team report and send to your DM
/gen team # Alias of /generate-report team
```
Expand Down Expand Up @@ -359,12 +367,18 @@ Filename date suffix uses Friday of the reporting week, e.g. `TEAMX_20260220.md`

### Listing Items

Anyone can view this week's items:
By default, `/list` shows only the caller's items for the current reporting week:

```
/list
```

Managers and members can use `/list all` to see the full team list:

```
/list all
```

`/list` now includes inline actions:
- Members can edit/delete only their own items.
- Managers can edit/delete all items.
Expand Down
3 changes: 3 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ llm_critic_enabled: false
# LLM API keys (set the one that matches llm_provider)
anthropic_api_key: "sk-ant-your-key"
openai_api_key: ""
# Optional base URL for OpenAI-compatible endpoints.
# Leave default for api.openai.com, or point to a lab-hosted endpoint such as gpt-oss.
openai_base_url: "https://api.openai.com/v1"

# Data and output paths
db_path: "./reportbot.db"
Expand Down
3 changes: 2 additions & 1 deletion internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func Main() {
cfg := config.LoadConfig()
appliedHTTPTimeout := httpx.ConfigureExternalHTTPClient(cfg.ExternalHTTPTimeoutSeconds)
log.Printf(
"Config loaded. Team=%s Managers=%d TeamMembers=%d Timezone=%s LLMBatchSize=%d LLMConfidenceThreshold=%.2f LLMExampleCount=%d LLMExampleMaxChars=%d LLMGlossaryPath=%s ExternalHTTPTimeout=%s",
"Config loaded. Team=%s Managers=%d TeamMembers=%d Timezone=%s LLMBatchSize=%d LLMConfidenceThreshold=%.2f LLMExampleCount=%d LLMExampleMaxChars=%d LLMGlossaryPath=%s OpenAIBaseURL=%s ExternalHTTPTimeout=%s",
cfg.TeamName,
len(cfg.ManagerSlackIDs),
len(cfg.TeamMembers),
Expand All @@ -27,6 +27,7 @@ func Main() {
cfg.LLMExampleCount,
cfg.LLMExampleMaxLen,
cfg.LLMGlossaryPath,
cfg.OpenAIBaseURL,
appliedHTTPTimeout,
)

Expand Down
8 changes: 8 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type Config struct {
ReportTemplatePath string `yaml:"report_template_path"`
AnthropicAPIKey string `yaml:"anthropic_api_key"`
OpenAIAPIKey string `yaml:"openai_api_key"`
OpenAIBaseURL string `yaml:"openai_base_url"`

DBPath string `yaml:"db_path"`
ReportOutputDir string `yaml:"report_output_dir"`
Expand Down Expand Up @@ -101,6 +102,7 @@ func LoadConfig() Config {
envOverride(&cfg.ReportTemplatePath, "REPORT_TEMPLATE_PATH")
envOverride(&cfg.AnthropicAPIKey, "ANTHROPIC_API_KEY")
envOverride(&cfg.OpenAIAPIKey, "OPENAI_API_KEY")
envOverride(&cfg.OpenAIBaseURL, "OPENAI_BASE_URL")
envOverride(&cfg.DBPath, "DB_PATH")
envOverride(&cfg.ReportOutputDir, "REPORT_OUTPUT_DIR")
envOverride(&cfg.ReportChannelID, "REPORT_CHANNEL_ID")
Expand Down Expand Up @@ -146,6 +148,9 @@ func LoadConfig() Config {
if cfg.DBPath == "" {
cfg.DBPath = "./reportbot.db"
}
if cfg.OpenAIBaseURL == "" {
cfg.OpenAIBaseURL = "https://api.openai.com/v1"
}
if cfg.ReportOutputDir == "" {
cfg.ReportOutputDir = "./reports"
}
Expand Down Expand Up @@ -246,6 +251,9 @@ func LoadConfig() Config {
if cfg.ExternalHTTPTimeoutSeconds < 5 {
log.Fatalf("invalid external_http_timeout_seconds '%d': must be >= 5", cfg.ExternalHTTPTimeoutSeconds)
}
if cfg.OpenAIBaseURL != "" {
cfg.OpenAIBaseURL = strings.TrimRight(cfg.OpenAIBaseURL, "/")
}
if cfg.LLMGlossaryPath != "" {
if err := validateGlossaryPath(cfg.LLMGlossaryPath); err != nil {
log.Fatalf("invalid llm_glossary_path '%s': %v", cfg.LLMGlossaryPath, err)
Expand Down
7 changes: 7 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func TestLoadConfigFromEnvWithDefaults(t *testing.T) {
if cfg.DBPath != "./reportbot.db" {
t.Fatalf("unexpected db path default: %q", cfg.DBPath)
}
if cfg.OpenAIBaseURL != "https://api.openai.com/v1" {
t.Fatalf("unexpected OpenAI base URL default: %q", cfg.OpenAIBaseURL)
}
if cfg.ReportOutputDir != "./reports" {
t.Fatalf("unexpected report output dir default: %q", cfg.ReportOutputDir)
}
Expand Down Expand Up @@ -77,6 +80,7 @@ external_http_timeout_seconds: 75
t.Setenv("CONFIG_PATH", cfgPath)
t.Setenv("LLM_PROVIDER", "openai")
t.Setenv("OPENAI_API_KEY", "sk-env")
t.Setenv("OPENAI_BASE_URL", "https://api.fazai.fortinet.com/v1/")
t.Setenv("TEAM_NAME", "Env Team")
t.Setenv("DB_PATH", "/tmp/env.db")
t.Setenv("EXTERNAL_HTTP_TIMEOUT_SECONDS", "120")
Expand All @@ -93,6 +97,9 @@ external_http_timeout_seconds: 75
if cfg.OpenAIAPIKey != "sk-env" {
t.Fatalf("expected openai key from env override")
}
if cfg.OpenAIBaseURL != "https://api.fazai.fortinet.com/v1" {
t.Fatalf("expected openai base URL from env override, got %q", cfg.OpenAIBaseURL)
}
if cfg.DBPath != "/tmp/env.db" {
t.Fatalf("expected db path from env override, got %q", cfg.DBPath)
}
Expand Down
Loading