Compare commits
3 Commits
docs/hooks
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 73e37df129 | |||
| 6a93c606b9 | |||
| 73078946f1 |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,26 +0,0 @@
|
|||||||
# Changelog — Plugin Legal AI
|
|
||||||
|
|
||||||
## [2026-05-17] שיפורי תשתית: Hooks, Scheduled Jobs, Per-Agent Validation
|
|
||||||
|
|
||||||
### שינויים ב-plugin-legal-ai
|
|
||||||
|
|
||||||
#### Webhook Hook — עדכוני סטטוס תיק
|
|
||||||
- נוסף `onWebhook()` handler לאירוע `case-status` מ-legal-ai
|
|
||||||
- כל שינוי סטטוס תיק (מכל endpoint) → תגובה אוטומטית בעברית על ה-issue המקושר ב-Paperclip
|
|
||||||
- בשינוי סטטוס `qa_failed` — CEO מתעורר אוטומטית עם הנחיות לתיקון
|
|
||||||
- נוספה הגנת payload + לוג אזהרה על שדות חסרים
|
|
||||||
|
|
||||||
#### Scheduled Jobs — עבודות רקע
|
|
||||||
- **`stale-case-reminder`** (כל יום 08:00): מזהה תיקים שלא עודכנו 3+ ימים → מוסיף תגובה אזהרה על ה-issue
|
|
||||||
- **`weekly-feedback-analysis`** (כל ראשון 19:00): מסכם פידבק יו"ר מהשבוע → CEO מעדכן `decision-lessons.md`
|
|
||||||
- ביצועים: בנייה חד-פעמית של `Map<caseNumber, {issueId, companyId}>` — O(companies × issues) במקום O(cases × companies × issues)
|
|
||||||
|
|
||||||
#### manifest / plugin.json
|
|
||||||
- נוספה capability: `webhooks.receive`
|
|
||||||
- נוספה הצהרת webhook: `{ endpointKey: "case-status" }`
|
|
||||||
- נוספו הגדרות ל-2 jobs חדשים
|
|
||||||
|
|
||||||
#### תיקוני Quality
|
|
||||||
- החלפת `let` ב-`const` (biome)
|
|
||||||
- תיקון סדר imports (biome)
|
|
||||||
- תיקון פורמט manifest.ts (biome)
|
|
||||||
@@ -713,7 +713,15 @@ const plugin = definePlugin({
|
|||||||
const apiBase =
|
const apiBase =
|
||||||
(config.legalApiBaseUrl as string) ?? "http://localhost:8085";
|
(config.legalApiBaseUrl as string) ?? "http://localhost:8085";
|
||||||
|
|
||||||
const resp = await ctx.http.fetch(`${apiBase}/api/cases/stale?days=3`);
|
let resp: Awaited<ReturnType<typeof ctx.http.fetch>>;
|
||||||
|
try {
|
||||||
|
resp = await ctx.http.fetch(`${apiBase}/api/cases/stale?days=3`);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.logger.error("stale-case-reminder: fetch failed", {
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
ctx.logger.error(`stale-case-reminder: API error ${resp.status}`);
|
ctx.logger.error(`stale-case-reminder: API error ${resp.status}`);
|
||||||
return;
|
return;
|
||||||
@@ -799,26 +807,39 @@ const plugin = definePlugin({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick the first company with a known CEO mapping — decision-lessons.md is
|
||||||
|
// shared between companies, so a single CEO invocation is sufficient.
|
||||||
const companies = await ctx.companies.list();
|
const companies = await ctx.companies.list();
|
||||||
const company = companies[0];
|
const mapped = companies
|
||||||
if (!company) return;
|
.map((c) => ({ company: c, ceoId: CEO_AGENT_IDS[c.id] }))
|
||||||
|
.filter((x): x is { company: (typeof companies)[0]; ceoId: string } =>
|
||||||
|
Boolean(x.ceoId),
|
||||||
|
);
|
||||||
|
|
||||||
const ceoId = CEO_AGENT_IDS[company.id];
|
if (mapped.length === 0) {
|
||||||
if (!ceoId) {
|
|
||||||
ctx.logger.warn(
|
ctx.logger.warn(
|
||||||
`weekly-feedback-analysis: no CEO agent for company ${company.id}`,
|
"weekly-feedback-analysis: no company has a mapped CEO agent — skipping",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { company, ceoId } = mapped[0];
|
||||||
|
|
||||||
|
try {
|
||||||
await ctx.agents.invoke(ceoId, company.id, {
|
await ctx.agents.invoke(ceoId, company.id, {
|
||||||
prompt: `ניתוח פידבק שבועי יו"ר (${data.entry_count} פריטים):\n\n${data.summary}\n\nהמשימה: עדכן את /home/chaim/legal-ai/docs/legal-decision-lessons.md עם הלקחים החדשים שעולים מהפידבק. הוסף רק לקחים חדשים שלא קיימים כבר. קבץ לפי נושא.`,
|
prompt: `ניתוח פידבק שבועי יו"ר (${data.entry_count} פריטים):\n\n${data.summary}\n\nהמשימה: עדכן את /home/chaim/legal-ai/docs/legal-decision-lessons.md עם הלקחים החדשים שעולים מהפידבק. הוסף רק לקחים חדשים שלא קיימים כבר. קבץ לפי נושא.`,
|
||||||
reason: "weekly-feedback-analysis scheduled job",
|
reason: "weekly-feedback-job",
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.logger.info(
|
ctx.logger.info(
|
||||||
`weekly-feedback-analysis: invoked CEO ${ceoId} with ${data.entry_count} feedback entries`,
|
`weekly-feedback-analysis: invoked CEO ${ceoId} (company ${company.id}) with ${data.entry_count} feedback entries`,
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
ctx.logger.error("weekly-feedback-analysis: failed to invoke CEO", {
|
||||||
|
error: String(err),
|
||||||
|
ceoId,
|
||||||
|
companyId: company.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.logger.info("Legal AI plugin ready");
|
ctx.logger.info("Legal AI plugin ready");
|
||||||
@@ -831,8 +852,27 @@ const plugin = definePlugin({
|
|||||||
async onWebhook(input: PluginWebhookInput): Promise<void> {
|
async onWebhook(input: PluginWebhookInput): Promise<void> {
|
||||||
if (!pluginCtx) return; // not yet initialized
|
if (!pluginCtx) return; // not yet initialized
|
||||||
|
|
||||||
// TODO: add idempotency guard using input.requestId to prevent duplicate
|
// Idempotency guard: skip duplicate deliveries within 5 minutes
|
||||||
// comments on rapid retries (store requestId in plugin state with ~60s TTL)
|
if (input.requestId) {
|
||||||
|
const idempKey = `webhook-idem-${input.requestId}`;
|
||||||
|
const seenAt = await pluginCtx.state.get({
|
||||||
|
scopeKind: "instance",
|
||||||
|
stateKey: idempKey,
|
||||||
|
});
|
||||||
|
if (seenAt && typeof seenAt === "string") {
|
||||||
|
const ageMs = Date.now() - new Date(seenAt).getTime();
|
||||||
|
if (ageMs < 5 * 60 * 1000) {
|
||||||
|
pluginCtx.logger.info(
|
||||||
|
`onWebhook: skipping duplicate requestId ${input.requestId} (age ${Math.round(ageMs / 1000)}s)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await pluginCtx.state.set(
|
||||||
|
{ scopeKind: "instance", stateKey: idempKey },
|
||||||
|
new Date().toISOString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { endpointKey, parsedBody } = input;
|
const { endpointKey, parsedBody } = input;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user