@ztimson/ai-agents 0.0.8 → 0.1.1
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.
- package/package.json +1 -1
- package/src/refine.mjs +83 -42
- package/src/review.mjs +4 -1
package/package.json
CHANGED
package/src/refine.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import * as dotenv from 'dotenv';
|
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
|
|
9
|
-
dotenv.config({quiet: true});
|
|
10
|
-
dotenv.config({path: '.env.local', override: true, quiet: true});
|
|
9
|
+
dotenv.config({quiet: true, debug: false});
|
|
10
|
+
dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
|
11
11
|
|
|
12
12
|
(async () => {
|
|
13
13
|
let p = process.argv[process.argv.length - 1];
|
|
@@ -21,22 +21,28 @@ dotenv.config({path: '.env.local', override: true, quiet: true});
|
|
|
21
21
|
ticket = process.env['TICKET'],
|
|
22
22
|
host = process.env['AI_HOST'],
|
|
23
23
|
model = process.env['AI_MODEL'],
|
|
24
|
+
labelDupe = process.env['LABELS_DUPE'] || 'Review/Duplicate',
|
|
25
|
+
labelEnabled = process.env['LABEL_ENABLED'] || 'Review/AI',
|
|
26
|
+
labelsReq = process.env['LABELS_REQ'] || 'Kind/Aesthetic,Kind/Bug,Kind/DevOps,Kind/Document,Kind/Enhancement,Kind/Refactor,Kind/Security',
|
|
27
|
+
labelsOpt = process.env['LABELS_OPT'] || 'Breaking,Priority,QA',
|
|
24
28
|
token = process.env['AI_TOKEN'];
|
|
25
29
|
|
|
26
30
|
console.log(`Processing issue #${ticket}`);
|
|
27
31
|
|
|
28
32
|
// Fetch issue
|
|
29
|
-
const
|
|
33
|
+
const issueData = await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
|
30
34
|
headers: {'Authorization': `token ${auth}`}
|
|
35
|
+
}).then(async resp => {
|
|
36
|
+
if(resp.ok) return resp.json();
|
|
37
|
+
else throw new Error(`${resp.status} ${await resp.text()}`);
|
|
31
38
|
});
|
|
32
|
-
if(
|
|
33
|
-
const issueData = await issueRes.json();
|
|
34
|
-
if(!issueData.labels?.some(l => l.name === 'Review/AI')) {
|
|
39
|
+
if(issueData.labels?.length !== 1 || issueData.labels[0]?.name !== labelEnabled) {
|
|
35
40
|
console.log('Skipping');
|
|
36
41
|
return process.exit();
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
// Gather readme & template
|
|
45
|
+
let title = '', labels = [], readme = '', readmeP = path.join(process.cwd(), 'README.md');
|
|
40
46
|
if(fs.existsSync(readmeP)) readme = fs.readFileSync(readmeP, 'utf-8');
|
|
41
47
|
const template = p ? fs.readFileSync(p, 'utf-8') : `## Description
|
|
42
48
|
|
|
@@ -80,6 +86,7 @@ Implementation details, constraints, dependencies, design decisions
|
|
|
80
86
|
| **Total** | **0-15** |
|
|
81
87
|
`;
|
|
82
88
|
|
|
89
|
+
// Create AI
|
|
83
90
|
let options = {ollama: {model, host}};
|
|
84
91
|
if(host === 'anthropic') options = {anthropic: {model, token}};
|
|
85
92
|
else if(host === 'openai') options = {openAi: {model, token}};
|
|
@@ -93,18 +100,27 @@ Implementation details, constraints, dependencies, design decisions
|
|
|
93
100
|
args: {title: {type: 'string', description: 'Ticket title, must match format: Module - Verb noun', required: true}},
|
|
94
101
|
fn: (args) => title = args.title
|
|
95
102
|
}, {
|
|
96
|
-
name: '
|
|
97
|
-
description: '
|
|
98
|
-
args: {
|
|
99
|
-
fn: (args) =>
|
|
103
|
+
name: 'add_label',
|
|
104
|
+
description: 'Add a label to the ticket',
|
|
105
|
+
args: {label: {type: 'string', description: 'Label name', required: true}},
|
|
106
|
+
fn: async (args) => {
|
|
107
|
+
labels.push(args.label);
|
|
108
|
+
return await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
111
|
+
body: `{"labels":["${args.label}"]}`
|
|
112
|
+
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
|
113
|
+
}
|
|
100
114
|
}],
|
|
101
115
|
system: `Transform raw tickets into structured markdown following the template EXACTLY.
|
|
102
116
|
|
|
103
117
|
**MANDATORY STEPS:**
|
|
104
|
-
1.
|
|
105
|
-
2.
|
|
106
|
-
3. Call \`
|
|
107
|
-
4.
|
|
118
|
+
1. Call \`title\` tool EXACTLY ONCE in format: "[Module] - [Verb] [subject]" (example: Storage - fix file uploads)
|
|
119
|
+
2. Identify one label from each group which best applies to the ticket: ${labelsReq.replaceAll(',', ', ')}
|
|
120
|
+
3. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
|
|
121
|
+
4. Filter the following labels to any that apply to this ticket: ${labelsOpt.replaceAll(',', ', ')}
|
|
122
|
+
5. Call the \`add_label\` tool ONCE FOR EVERY LABEL identified in the previous step
|
|
123
|
+
6. Output the new ticket description in formatted markdown matching the following rules:
|
|
108
124
|
|
|
109
125
|
**TEMPLATE RULES:**
|
|
110
126
|
- Use ## headers (match template exactly)
|
|
@@ -125,11 +141,11 @@ Implementation details, constraints, dependencies, design decisions
|
|
|
125
141
|
| **Total** | **0-15** |
|
|
126
142
|
|
|
127
143
|
**SCORING:**
|
|
128
|
-
- Size: # of modules/layers/files
|
|
144
|
+
- Size: # of modules/layers/files affected
|
|
129
145
|
- Complexity: Technical difficulty
|
|
130
146
|
- Unknowns: Research/uncertainty needed
|
|
131
147
|
|
|
132
|
-
**README:**
|
|
148
|
+
**PROJECT README:**
|
|
133
149
|
\`\`\`markdown
|
|
134
150
|
${readme.trim() || 'No README available'}
|
|
135
151
|
\`\`\`
|
|
@@ -141,35 +157,60 @@ ${template.trim()}
|
|
|
141
157
|
|
|
142
158
|
Output ONLY markdown. No explanations, labels, or extra formatting.`});
|
|
143
159
|
|
|
160
|
+
// Format ticket with AI
|
|
144
161
|
const messages = await ai.language.ask(`Title: ${issueData.title}\n\nDescription:\n${issueData.body || 'No description provided'}`).catch(() => []);
|
|
145
162
|
const body = messages?.pop()?.content;
|
|
146
|
-
if(!body)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
const
|
|
151
|
-
method: '
|
|
152
|
-
headers: {
|
|
153
|
-
'Authorization': `token ${auth}`,
|
|
154
|
-
'Content-Type': 'application/json'
|
|
155
|
-
},
|
|
163
|
+
if(!body) throw new Error('Invalid response from AI');
|
|
164
|
+
|
|
165
|
+
// Check for duplicates
|
|
166
|
+
const repoInfo = await fetch(`${git}/api/v1/repos/${owner}/${repo}`, {headers: {'Authorization': `token ${auth}`},}).then(resp => resp.ok ? resp.json() : null);
|
|
167
|
+
const search = await fetch(`${git}/api/v1/repos/issues/search`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
156
170
|
body: JSON.stringify({
|
|
157
|
-
|
|
158
|
-
|
|
171
|
+
owner,
|
|
172
|
+
priority_repo_id: repoInfo.id,
|
|
173
|
+
type: 'issues',
|
|
174
|
+
limit: 3,
|
|
175
|
+
q: title
|
|
159
176
|
})
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
}).then(resp => resp.ok ? resp.json() : []);
|
|
178
|
+
let dupeId = null;
|
|
179
|
+
const dupeIds = search.map(t => t.id);
|
|
180
|
+
const dupes = search.map(t => `ID: ${t.id}\nTitle: ${t.title}\n\`\`\`markdown\n${t.body}\n\`\`\``).join('\n\n');
|
|
181
|
+
const hasDuplicates = (await ai.language.ask(`ID: ${issueData.id}\nTitle: ${title}\n\`\`\`markdown\n${body}\n\`\`\``, {
|
|
182
|
+
system: `Your job is to identify duplicates. Respond ONLY with the duplicate's ID number or "NONE" if no match exists\n\n${dupes}`
|
|
183
|
+
}))?.pop()?.content;
|
|
184
|
+
// Handle duplicates
|
|
185
|
+
if(hasDuplicates && hasDuplicates !== 'NONE' && (dupeId = dupeIds.find(id => id == hasDuplicates.trim()))) {
|
|
186
|
+
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/comments`, {
|
|
164
187
|
method: 'POST',
|
|
165
|
-
headers: {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
189
|
+
body: `{"body": "Duplicate of #${dupeId}"}`
|
|
190
|
+
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
|
191
|
+
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}/labels`, {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
194
|
+
body: `{"labels":["${labelDupe}"]}`
|
|
195
|
+
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
|
196
|
+
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
|
197
|
+
method: 'PATCH',
|
|
198
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
199
|
+
body: '{"state": "closed"}'
|
|
200
|
+
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
|
201
|
+
console.log('Duplicate');
|
|
202
|
+
return process.exit();
|
|
172
203
|
}
|
|
173
204
|
|
|
174
|
-
|
|
175
|
-
}
|
|
205
|
+
// Update ticket
|
|
206
|
+
await fetch(`${git}/api/v1/repos/${owner}/${repo}/issues/${ticket}`, {
|
|
207
|
+
method: 'PATCH',
|
|
208
|
+
headers: {'Authorization': `token ${auth}`, 'Content-Type': 'application/json'},
|
|
209
|
+
body: JSON.stringify({title, body})
|
|
210
|
+
}).then(async resp => { if(!resp.ok) throw new Error(`${resp.status} ${await resp.text()}`); });
|
|
211
|
+
|
|
212
|
+
console.log(`Title: ${title}\nLabels: ${labels.join(', ')}\nBody:\n${body}`);
|
|
213
|
+
})().catch(err => {
|
|
214
|
+
console.error(`Error: ${err.message || err.toString()}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
package/src/review.mjs
CHANGED
|
@@ -106,4 +106,7 @@ dotenv.config({path: '.env.local', override: true, quiet: true, debug: false});
|
|
|
106
106
|
if(!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
|
107
107
|
}
|
|
108
108
|
console.log(comments.map(c => `${c.path}${c.new_position ? `:${c.new_position}` : ''}\n${c.body}`).join('\n\n') + '\n\n' + summary);
|
|
109
|
-
})()
|
|
109
|
+
})().catch(err => {
|
|
110
|
+
console.error(`Error: ${err.message || err.toString()}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|