strands-issue-writer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- strands_issue_writer-0.1.0/.github/workflows/ci.yml +21 -0
- strands_issue_writer-0.1.0/.github/workflows/publish.yml +61 -0
- strands_issue_writer-0.1.0/.gitignore +14 -0
- strands_issue_writer-0.1.0/LICENSE +21 -0
- strands_issue_writer-0.1.0/Modelfile.example +10 -0
- strands_issue_writer-0.1.0/PKG-INFO +210 -0
- strands_issue_writer-0.1.0/README.md +174 -0
- strands_issue_writer-0.1.0/docs/SERVING.md +201 -0
- strands_issue_writer-0.1.0/pyproject.toml +62 -0
- strands_issue_writer-0.1.0/strands-issue-writer-logo.svg +38 -0
- strands_issue_writer-0.1.0/strands_issue_writer/__init__.py +18 -0
- strands_issue_writer-0.1.0/strands_issue_writer/agent.py +48 -0
- strands_issue_writer-0.1.0/strands_issue_writer/cli.py +114 -0
- strands_issue_writer-0.1.0/strands_issue_writer/dashboard/__init__.py +1 -0
- strands_issue_writer-0.1.0/strands_issue_writer/dashboard/index.html +204 -0
- strands_issue_writer-0.1.0/strands_issue_writer/dashboard/server.py +89 -0
- strands_issue_writer-0.1.0/strands_issue_writer/models.py +73 -0
- strands_issue_writer-0.1.0/strands_issue_writer/prompts.py +41 -0
- strands_issue_writer-0.1.0/strands_issue_writer/provider.py +81 -0
- strands_issue_writer-0.1.0/strands_issue_writer/tools/__init__.py +6 -0
- strands_issue_writer-0.1.0/strands_issue_writer/tools/draft.py +78 -0
- strands_issue_writer-0.1.0/strands_issue_writer/tools/inbox.py +37 -0
- strands_issue_writer-0.1.0/strands_issue_writer/tools/review.py +74 -0
- strands_issue_writer-0.1.0/strands_issue_writer/tools/tracker.py +104 -0
- strands_issue_writer-0.1.0/tests/test_models.py +61 -0
- strands_issue_writer-0.1.0/tests/test_provider.py +24 -0
- strands_issue_writer-0.1.0/tests/test_review.py +65 -0
- strands_issue_writer-0.1.0/tests/test_tracker.py +40 -0
- strands_issue_writer-0.1.0/uv.lock +2206 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
with:
|
|
15
|
+
enable-cache: true
|
|
16
|
+
- run: uv python install 3.12
|
|
17
|
+
- run: uv sync --group dev --extra ollama --extra vllm --extra dashboard
|
|
18
|
+
- run: uv run ruff check .
|
|
19
|
+
# No model is served in CI; these tests cover the contract, the rule
|
|
20
|
+
# review and the tracker payloads, all of which are deterministic.
|
|
21
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishing is deliberate: tag a release, or run it by hand.
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
target:
|
|
8
|
+
description: "Where to publish"
|
|
9
|
+
type: choice
|
|
10
|
+
options: [testpypi, pypi]
|
|
11
|
+
default: testpypi
|
|
12
|
+
push:
|
|
13
|
+
tags: ["v*"]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: astral-sh/setup-uv@v5
|
|
21
|
+
- run: uv python install 3.12
|
|
22
|
+
|
|
23
|
+
# Never publish something that does not pass its own gate.
|
|
24
|
+
- run: uv sync --group dev --extra ollama --extra vllm --extra dashboard
|
|
25
|
+
- run: uv run ruff check .
|
|
26
|
+
- run: uv run pytest
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Check metadata
|
|
32
|
+
run: uvx twine check dist/*
|
|
33
|
+
|
|
34
|
+
- uses: actions/upload-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
needs: build
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
environment: ${{ github.event.inputs.target || 'pypi' }}
|
|
43
|
+
# Trusted publishing: no API token to store or rotate. Configure the
|
|
44
|
+
# publisher once at pypi.org/manage/account/publishing/
|
|
45
|
+
permissions:
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/download-artifact@v4
|
|
49
|
+
with:
|
|
50
|
+
name: dist
|
|
51
|
+
path: dist/
|
|
52
|
+
|
|
53
|
+
- name: Publish to TestPyPI
|
|
54
|
+
if: ${{ github.event.inputs.target == 'testpypi' }}
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
if: ${{ github.event.inputs.target == 'pypi' || startsWith(github.ref, 'refs/tags/v') }}
|
|
61
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furkan Portakal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Example Modelfile for Ollama. Adjust the FROM path to your downloaded GGUF.
|
|
2
|
+
FROM ./model/issue-writer-gemma4.q4_k_m.gguf
|
|
3
|
+
|
|
4
|
+
# The model fills a fixed schema; sampling only produces malformed JSON.
|
|
5
|
+
PARAMETER temperature 0
|
|
6
|
+
PARAMETER top_p 1
|
|
7
|
+
PARAMETER num_ctx 4096
|
|
8
|
+
PARAMETER num_predict 2048
|
|
9
|
+
|
|
10
|
+
SYSTEM """You are a senior agile delivery assistant. You turn raw product input into well-formed issues. Reply with a single valid JSON object and nothing else. Follow INVEST, write testable Given/When/Then acceptance criteria, and never invent facts: anything the input does not state goes into `assumptions` or `clarifying_questions`."""
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: strands-issue-writer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Strands agent that turns raw product chatter into well-formed issue tracker entries, powered by a locally served fine-tuned model
|
|
5
|
+
Project-URL: Homepage, https://github.com/fport/strands-issue-writer
|
|
6
|
+
Project-URL: Dataset, https://huggingface.co/datasets/fport/issue-writer-tr-en
|
|
7
|
+
Project-URL: Training, https://github.com/fport/issue-writer
|
|
8
|
+
Project-URL: Issues, https://github.com/fport/strands-issue-writer/issues
|
|
9
|
+
Author: Furkan Portakal
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,issue-tracker,jira,llm,lora,ollama,strands,vllm
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic>=2.9
|
|
22
|
+
Requires-Dist: strands-agents>=1.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: fastapi>=0.115; extra == 'all'
|
|
25
|
+
Requires-Dist: ollama>=0.4; extra == 'all'
|
|
26
|
+
Requires-Dist: openai>=1.50; extra == 'all'
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == 'all'
|
|
28
|
+
Provides-Extra: dashboard
|
|
29
|
+
Requires-Dist: fastapi>=0.115; extra == 'dashboard'
|
|
30
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == 'dashboard'
|
|
31
|
+
Provides-Extra: ollama
|
|
32
|
+
Requires-Dist: ollama>=0.4; extra == 'ollama'
|
|
33
|
+
Provides-Extra: vllm
|
|
34
|
+
Requires-Dist: openai>=1.50; extra == 'vllm'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
<img src="strands-issue-writer-logo.svg" alt="strands-issue-writer" width="180">
|
|
39
|
+
<h1>strands-issue-writer</h1>
|
|
40
|
+
<p><strong>Turn product chatter into well-formed issues.</strong> Locally served, rule-checked, never invented.</p>
|
|
41
|
+
<p>
|
|
42
|
+
<a href="https://pypi.org/project/strands-issue-writer/"><img alt="PyPI" src="https://img.shields.io/pypi/v/strands-issue-writer?style=flat-square&color=F97316"></a>
|
|
43
|
+
<a href="https://pypi.org/project/strands-issue-writer/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/strands-issue-writer?style=flat-square"></a>
|
|
44
|
+
<a href="https://github.com/fport/strands-issue-writer/actions"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/fport/strands-issue-writer/ci.yml?style=flat-square&label=CI"></a>
|
|
45
|
+
<a href="https://huggingface.co/datasets/fport/issue-writer-tr-en"><img alt="Dataset" src="https://img.shields.io/badge/%F0%9F%A4%97%20dataset-issue--writer--tr--en-yellow?style=flat-square"></a>
|
|
46
|
+
<a href="https://github.com/fport/strands-issue-writer/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-green?style=flat-square"></a>
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
A [Strands](https://strandsagents.com/) agent that turns raw product chatter —
|
|
53
|
+
Slack messages, support tickets, Sentry alerts, meeting notes — into well-formed
|
|
54
|
+
issue tracker entries, using a **fine-tuned model you serve yourself**.
|
|
55
|
+
|
|
56
|
+
Nothing leaves your machine. The model is the one trained in
|
|
57
|
+
[fport/issue-writer](https://github.com/fport/issue-writer) on
|
|
58
|
+
[fport/issue-writer-tr-en](https://huggingface.co/datasets/fport/issue-writer-tr-en),
|
|
59
|
+
served through Ollama or vLLM.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ strands-issue-writer draft "selam, müşteriler fatura geçmişini tek tek açmak
|
|
63
|
+
yerine toplu PDF olarak indirmek istiyor. muhasebeciler ayda 30-40 fatura
|
|
64
|
+
indiriyor, çok vakit alıyor. bu sprint yetişir mi?"
|
|
65
|
+
|
|
66
|
+
[Story] Fatura geçmişini tek PDF olarak dışa aktarma ekle
|
|
67
|
+
priority Medium · 5 points
|
|
68
|
+
components: Billing labels: export, self-service
|
|
69
|
+
|
|
70
|
+
h2. Kullanıcı Hikâyesi
|
|
71
|
+
Bir muhasebe işlerini yürüten müşteri olarak fatura geçmişimi tek bir PDF olarak
|
|
72
|
+
indirmek istiyorum; böylece her faturayı tek tek açmak zorunda kalmayayım.
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
Assumptions:
|
|
76
|
+
- Dışa aktarma kapsamının seçilen tarih aralığı olduğu varsayıldı; girdide sınır
|
|
77
|
+
belirtilmedi.
|
|
78
|
+
Open questions:
|
|
79
|
+
- İndirilebilecek en geniş tarih aralığı ne olmalı?
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Two models, two jobs
|
|
83
|
+
|
|
84
|
+
The distinction matters and it is easy to get wrong:
|
|
85
|
+
|
|
86
|
+
**The writer** is the fine-tuned model behind `draft_issue`. It is good at exactly
|
|
87
|
+
one thing — turning messy input into a structured issue that obeys the writing
|
|
88
|
+
rules. It is not a general assistant and should not be asked to reason about your
|
|
89
|
+
backlog.
|
|
90
|
+
|
|
91
|
+
**The orchestrator** runs the Strands reasoning loop: which tool to call, whether a
|
|
92
|
+
draft is good enough, when to ask instead of guess. By default it is the same local
|
|
93
|
+
model, which keeps everything offline. Give it a stronger model when you want better
|
|
94
|
+
judgement about pushing back on a draft:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from strands.models.anthropic import AnthropicModel
|
|
98
|
+
from strands_issue_writer import build_agent
|
|
99
|
+
|
|
100
|
+
agent = build_agent(orchestrator_model=AnthropicModel(model_id="claude-opus-5"))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The writer stays local either way — your product conversations never leave.
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install "strands-issue-writer[ollama,dashboard]" # or [vllm,dashboard]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
From source, for development:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/fport/strands-issue-writer && cd strands-issue-writer
|
|
115
|
+
uv sync --group dev --extra ollama --extra dashboard
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then serve the model. [`docs/SERVING.md`](docs/SERVING.md) covers going from a
|
|
119
|
+
trained LoRA adapter to a running endpoint; the short version:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
huggingface-cli download fport/issue-writer-gemma4-gguf --include "*q4_k_m.gguf" --local-dir ./model
|
|
123
|
+
ollama create issue-writer -f Modelfile.example
|
|
124
|
+
export ISSUE_WRITER_PROVIDER=ollama ISSUE_WRITER_MODEL=issue-writer
|
|
125
|
+
strands-issue-writer doctor
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Use
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
strands-issue-writer doctor # is the endpoint alive, right model?
|
|
132
|
+
strands-issue-writer draft "…" [--json] # one issue, rendered or as JSON
|
|
133
|
+
strands-issue-writer agent # interactive agent with all tools
|
|
134
|
+
strands-issue-writer dashboard # web UI on :8765
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
As a library:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from strands_issue_writer import build_agent
|
|
141
|
+
|
|
142
|
+
agent = build_agent()
|
|
143
|
+
agent("Read inbox.jsonl and draft issues for anything that looks like a bug. "
|
|
144
|
+
"Show me each one, do not push anything.")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Tools
|
|
148
|
+
|
|
149
|
+
| Tool | What it does |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `draft_issue` | raw text → validated `Issue` via the local writer model |
|
|
152
|
+
| `review_issue` | rule checks: schema, sections, reproduction steps, invented facts, criteria observability |
|
|
153
|
+
| `render_issue` | human-readable rendering for review before anything is published |
|
|
154
|
+
| `push_to_tracker` | creates the issue — **dry run unless `confirm=True`** |
|
|
155
|
+
| `read_inbox` | reads pending raw inputs from a `.jsonl` or `---`-separated file |
|
|
156
|
+
|
|
157
|
+
`review_issue` is deliberately **not** model-based. These are checks a rule can
|
|
158
|
+
make, so a rule makes them: deterministically, on every draft, for free. It catches
|
|
159
|
+
the failure that matters most with a small local model — inventing a version number
|
|
160
|
+
or a metric that was never in the input.
|
|
161
|
+
|
|
162
|
+
`push_to_tracker` refuses to publish without explicit confirmation. Creating tickets
|
|
163
|
+
in someone's tracker is an outward action; it should not happen because a model
|
|
164
|
+
decided it was helpful.
|
|
165
|
+
|
|
166
|
+
## Dashboard
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
strands-issue-writer dashboard
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Three panels answering the three questions you actually have while a local model
|
|
173
|
+
drafts for you:
|
|
174
|
+
|
|
175
|
+
- **left** — the raw input, and a history of what you have drafted this session
|
|
176
|
+
- **centre** — the issue as it came out: type, summary, chips for priority and
|
|
177
|
+
components, the rendered body
|
|
178
|
+
- **right** — the rule review. Green when it passes, and when it does not, the
|
|
179
|
+
violations by name. Below that, the model's own assumptions and open questions,
|
|
180
|
+
which is where you find out whether it filled a gap honestly or silently.
|
|
181
|
+
|
|
182
|
+
The header shows which provider and model answered, and how long it took. A local
|
|
183
|
+
4B model on a laptop runs about 3–8 seconds per issue.
|
|
184
|
+
|
|
185
|
+
## Configuration
|
|
186
|
+
|
|
187
|
+
| Variable | Default | Meaning |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `ISSUE_WRITER_PROVIDER` | `ollama` | `ollama`, `vllm`, or `openai` |
|
|
190
|
+
| `ISSUE_WRITER_MODEL` | `issue-writer` | served model name |
|
|
191
|
+
| `ISSUE_WRITER_HOST` | `http://localhost:11434` | Ollama host |
|
|
192
|
+
| `ISSUE_WRITER_BASE_URL` | `http://localhost:8000/v1` | OpenAI-compatible endpoint |
|
|
193
|
+
| `ISSUE_WRITER_TEMPERATURE` | `0` | leave it there; the output is a schema |
|
|
194
|
+
| `TRACKER_URL` / `TRACKER_EMAIL` / `TRACKER_TOKEN` | — | only needed to actually publish |
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
uv sync --group dev
|
|
200
|
+
uv run pytest
|
|
201
|
+
uv run ruff check .
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The test suite runs without a model: the contract, the rule review and the tracker
|
|
205
|
+
payloads are all deterministic. Only `draft_issue` needs a served endpoint, and
|
|
206
|
+
`doctor` tells you whether you have one.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="strands-issue-writer-logo.svg" alt="strands-issue-writer" width="180">
|
|
3
|
+
<h1>strands-issue-writer</h1>
|
|
4
|
+
<p><strong>Turn product chatter into well-formed issues.</strong> Locally served, rule-checked, never invented.</p>
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://pypi.org/project/strands-issue-writer/"><img alt="PyPI" src="https://img.shields.io/pypi/v/strands-issue-writer?style=flat-square&color=F97316"></a>
|
|
7
|
+
<a href="https://pypi.org/project/strands-issue-writer/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/strands-issue-writer?style=flat-square"></a>
|
|
8
|
+
<a href="https://github.com/fport/strands-issue-writer/actions"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/fport/strands-issue-writer/ci.yml?style=flat-square&label=CI"></a>
|
|
9
|
+
<a href="https://huggingface.co/datasets/fport/issue-writer-tr-en"><img alt="Dataset" src="https://img.shields.io/badge/%F0%9F%A4%97%20dataset-issue--writer--tr--en-yellow?style=flat-square"></a>
|
|
10
|
+
<a href="https://github.com/fport/strands-issue-writer/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-green?style=flat-square"></a>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
A [Strands](https://strandsagents.com/) agent that turns raw product chatter —
|
|
17
|
+
Slack messages, support tickets, Sentry alerts, meeting notes — into well-formed
|
|
18
|
+
issue tracker entries, using a **fine-tuned model you serve yourself**.
|
|
19
|
+
|
|
20
|
+
Nothing leaves your machine. The model is the one trained in
|
|
21
|
+
[fport/issue-writer](https://github.com/fport/issue-writer) on
|
|
22
|
+
[fport/issue-writer-tr-en](https://huggingface.co/datasets/fport/issue-writer-tr-en),
|
|
23
|
+
served through Ollama or vLLM.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
$ strands-issue-writer draft "selam, müşteriler fatura geçmişini tek tek açmak
|
|
27
|
+
yerine toplu PDF olarak indirmek istiyor. muhasebeciler ayda 30-40 fatura
|
|
28
|
+
indiriyor, çok vakit alıyor. bu sprint yetişir mi?"
|
|
29
|
+
|
|
30
|
+
[Story] Fatura geçmişini tek PDF olarak dışa aktarma ekle
|
|
31
|
+
priority Medium · 5 points
|
|
32
|
+
components: Billing labels: export, self-service
|
|
33
|
+
|
|
34
|
+
h2. Kullanıcı Hikâyesi
|
|
35
|
+
Bir muhasebe işlerini yürüten müşteri olarak fatura geçmişimi tek bir PDF olarak
|
|
36
|
+
indirmek istiyorum; böylece her faturayı tek tek açmak zorunda kalmayayım.
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
Assumptions:
|
|
40
|
+
- Dışa aktarma kapsamının seçilen tarih aralığı olduğu varsayıldı; girdide sınır
|
|
41
|
+
belirtilmedi.
|
|
42
|
+
Open questions:
|
|
43
|
+
- İndirilebilecek en geniş tarih aralığı ne olmalı?
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Two models, two jobs
|
|
47
|
+
|
|
48
|
+
The distinction matters and it is easy to get wrong:
|
|
49
|
+
|
|
50
|
+
**The writer** is the fine-tuned model behind `draft_issue`. It is good at exactly
|
|
51
|
+
one thing — turning messy input into a structured issue that obeys the writing
|
|
52
|
+
rules. It is not a general assistant and should not be asked to reason about your
|
|
53
|
+
backlog.
|
|
54
|
+
|
|
55
|
+
**The orchestrator** runs the Strands reasoning loop: which tool to call, whether a
|
|
56
|
+
draft is good enough, when to ask instead of guess. By default it is the same local
|
|
57
|
+
model, which keeps everything offline. Give it a stronger model when you want better
|
|
58
|
+
judgement about pushing back on a draft:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from strands.models.anthropic import AnthropicModel
|
|
62
|
+
from strands_issue_writer import build_agent
|
|
63
|
+
|
|
64
|
+
agent = build_agent(orchestrator_model=AnthropicModel(model_id="claude-opus-5"))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The writer stays local either way — your product conversations never leave.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "strands-issue-writer[ollama,dashboard]" # or [vllm,dashboard]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
From source, for development:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/fport/strands-issue-writer && cd strands-issue-writer
|
|
79
|
+
uv sync --group dev --extra ollama --extra dashboard
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Then serve the model. [`docs/SERVING.md`](docs/SERVING.md) covers going from a
|
|
83
|
+
trained LoRA adapter to a running endpoint; the short version:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
huggingface-cli download fport/issue-writer-gemma4-gguf --include "*q4_k_m.gguf" --local-dir ./model
|
|
87
|
+
ollama create issue-writer -f Modelfile.example
|
|
88
|
+
export ISSUE_WRITER_PROVIDER=ollama ISSUE_WRITER_MODEL=issue-writer
|
|
89
|
+
strands-issue-writer doctor
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Use
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
strands-issue-writer doctor # is the endpoint alive, right model?
|
|
96
|
+
strands-issue-writer draft "…" [--json] # one issue, rendered or as JSON
|
|
97
|
+
strands-issue-writer agent # interactive agent with all tools
|
|
98
|
+
strands-issue-writer dashboard # web UI on :8765
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
As a library:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from strands_issue_writer import build_agent
|
|
105
|
+
|
|
106
|
+
agent = build_agent()
|
|
107
|
+
agent("Read inbox.jsonl and draft issues for anything that looks like a bug. "
|
|
108
|
+
"Show me each one, do not push anything.")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Tools
|
|
112
|
+
|
|
113
|
+
| Tool | What it does |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `draft_issue` | raw text → validated `Issue` via the local writer model |
|
|
116
|
+
| `review_issue` | rule checks: schema, sections, reproduction steps, invented facts, criteria observability |
|
|
117
|
+
| `render_issue` | human-readable rendering for review before anything is published |
|
|
118
|
+
| `push_to_tracker` | creates the issue — **dry run unless `confirm=True`** |
|
|
119
|
+
| `read_inbox` | reads pending raw inputs from a `.jsonl` or `---`-separated file |
|
|
120
|
+
|
|
121
|
+
`review_issue` is deliberately **not** model-based. These are checks a rule can
|
|
122
|
+
make, so a rule makes them: deterministically, on every draft, for free. It catches
|
|
123
|
+
the failure that matters most with a small local model — inventing a version number
|
|
124
|
+
or a metric that was never in the input.
|
|
125
|
+
|
|
126
|
+
`push_to_tracker` refuses to publish without explicit confirmation. Creating tickets
|
|
127
|
+
in someone's tracker is an outward action; it should not happen because a model
|
|
128
|
+
decided it was helpful.
|
|
129
|
+
|
|
130
|
+
## Dashboard
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
strands-issue-writer dashboard
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Three panels answering the three questions you actually have while a local model
|
|
137
|
+
drafts for you:
|
|
138
|
+
|
|
139
|
+
- **left** — the raw input, and a history of what you have drafted this session
|
|
140
|
+
- **centre** — the issue as it came out: type, summary, chips for priority and
|
|
141
|
+
components, the rendered body
|
|
142
|
+
- **right** — the rule review. Green when it passes, and when it does not, the
|
|
143
|
+
violations by name. Below that, the model's own assumptions and open questions,
|
|
144
|
+
which is where you find out whether it filled a gap honestly or silently.
|
|
145
|
+
|
|
146
|
+
The header shows which provider and model answered, and how long it took. A local
|
|
147
|
+
4B model on a laptop runs about 3–8 seconds per issue.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Meaning |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `ISSUE_WRITER_PROVIDER` | `ollama` | `ollama`, `vllm`, or `openai` |
|
|
154
|
+
| `ISSUE_WRITER_MODEL` | `issue-writer` | served model name |
|
|
155
|
+
| `ISSUE_WRITER_HOST` | `http://localhost:11434` | Ollama host |
|
|
156
|
+
| `ISSUE_WRITER_BASE_URL` | `http://localhost:8000/v1` | OpenAI-compatible endpoint |
|
|
157
|
+
| `ISSUE_WRITER_TEMPERATURE` | `0` | leave it there; the output is a schema |
|
|
158
|
+
| `TRACKER_URL` / `TRACKER_EMAIL` / `TRACKER_TOKEN` | — | only needed to actually publish |
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
uv sync --group dev
|
|
164
|
+
uv run pytest
|
|
165
|
+
uv run ruff check .
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The test suite runs without a model: the contract, the rule review and the tracker
|
|
169
|
+
payloads are all deterministic. Only `draft_issue` needs a served endpoint, and
|
|
170
|
+
`doctor` tells you whether you have one.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT.
|