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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Reaction for the `opportunity-matcher` watcher.
*
* Runs every 12h after the LLM scans member activity and produces a list of
* suggested matches. Persists each match as a `community_match` event so
* downstream consumers (intro-drafting agents, weekly digest, audit log) can
* iterate over a single source of truth instead of re-running the matcher.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface MatchData {
signals?: Array<{
member_a: string;
member_b: string;
reason: string;
confidence?: number;
}>;
Comment on lines +11 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align reaction payload fields with the watcher extraction schema.

MatchData expects member_a/member_b, but the opportunity-matcher watcher schema emits interested_members + suggested_action. This will persist records with missing participant names at runtime.

Suggested alignment patch
 interface MatchData {
   signals?: Array<{
-    member_a: string;
-    member_b: string;
+    interested_members?: string[];
     reason: string;
-    confidence?: number;
+    suggested_action?: string;
   }>;
 }

 export default async (ctx: ReactionContext, client: any): Promise<void> => {
@@
   for (const s of signals) {
+    const [member_a, member_b] = s.interested_members ?? [];
+    if (!member_a || !member_b) continue;
+
     await client.knowledge.save({
       entity_ids: ctx.entities.map((e) => e.id),
-      content: `Match: ${s.member_a} ↔ ${s.member_b} — ${s.reason}`,
+      content: `Match: ${member_a} ↔ ${member_b} — ${s.reason}`,
       semantic_type: "community_match",
       metadata: {
-        member_a: s.member_a,
-        member_b: s.member_b,
-        confidence: s.confidence ?? null,
+        member_a,
+        member_b,
+        suggested_action: s.suggested_action ?? null,
         window_id: ctx.window.id,
       },
     });
   }
 };

Also applies to: 28-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/agent-community/models/reactions/opportunity-matcher.reaction.ts`
around lines 11 - 17, The MatchData interface currently defines
member_a/member_b but the watcher emits interested_members and suggested_action;
update the MatchData type (and any code that constructs/persists it) to match
the watcher schema by replacing member_a/member_b with interested_members:
Array<{ /* participant fields from watcher */ }> and a suggested_action field
(including reason/confidence as provided), or alternatively add a transformation
when building MatchData that extracts participant names from interested_members
into member_a/member_b before saving; make this change for the MatchData
definition and every usage site (e.g., the code that creates match records in
opportunity-matcher.reaction and the related construction around lines where
MatchData is used) so stored records contain the actual participant names and
suggested_action data.

}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as MatchData;
const signals = data.signals ?? [];
if (signals.length === 0) return;

for (const s of signals) {
await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: `Match: ${s.member_a} ↔ ${s.member_b} — ${s.reason}`,
semantic_type: "community_match",
metadata: {
member_a: s.member_a,
member_b: s.member_b,
confidence: s.confidence ?? null,
window_id: ctx.window.id,
},
});
}
};
4 changes: 4 additions & 0 deletions examples/agent-community/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ watchers:
agent: agent-community
name: Opportunity matcher
schedule: 0 */12 * * *
notification_priority: normal
tags: [community, matching]
min_cooldown_seconds: 300
reaction_script: ./reactions/opportunity-matcher.reaction.ts
prompt: |
Monitor connected profiles, newsletters, websites, and member updates for new launches, posts, hiring signals, funding news, and project changes. Identify which members are likely to care, explain why, and queue approved intro or outreach drafts.
extraction_schema:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Reaction for atlas's `catalog-staleness-checker` watcher.
*
* Writes a `catalog_stale` event per stale entry the LLM identified. Atlas is
* a long-lived reference catalog — entries that haven't been re-verified in
* 90+ days are flagged so a curator can decide whether to refresh, retire, or
* leave them.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface StaleData {
stale_entries?: Array<{
entity_type: string;
slug: string;
last_updated: string;
suggested_action: string;
}>;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as StaleData;
const stale = data.stale_entries ?? [];
if (stale.length === 0) return;

for (const s of stale) {
await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: `Stale ${s.entity_type}/${s.slug} — last updated ${s.last_updated}\n→ ${s.suggested_action}`,
semantic_type: "catalog_stale",
metadata: {
entity_type: s.entity_type,
slug: s.slug,
last_updated: s.last_updated,
window_id: ctx.window.id,
},
});
}
};
40 changes: 40 additions & 0 deletions examples/atlas/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,43 @@ entities:
homepage_url:
type: string
format: uri

watchers:
# Demo watcher exercising the file-first apply surface: tags, notification
# routing, cooldowns, and a sibling reaction script. Atlas is a reference
# catalog (cities, countries, industries, universities, …) — once a week
# we sweep for entries that haven't been touched in 90+ days and flag them
# for re-verification.
- slug: catalog-staleness-checker
agent: atlas-curator
name: Catalog staleness checker
schedule: 0 4 * * 1
notification_priority: low
tags: [atlas, reference, weekly]
min_cooldown_seconds: 3600
reaction_script: ./reactions/catalog-staleness-checker.reaction.ts
prompt: |
Sweep the atlas reference catalog for entries that haven't been
updated in 90+ days. List the stalest 10 across cities, countries,
industries, technologies, and universities. Suggest a re-verification
action for each (e.g. "country/PL: confirm population from latest census").
extraction_schema:
type: object
required:
- stale_entries
properties:
stale_entries:
type: array
items:
type: object
properties:
entity_type:
type: string
slug:
type: string
last_updated:
type: string
suggested_action:
type: string
total_stale_count:
type: integer
4 changes: 4 additions & 0 deletions examples/delivery/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ watchers:
agent: delivery
name: Phoenix rollout tracker
schedule: 0 9 * * 1
notification_priority: high
notification_channel: both
tags: [delivery, weekly, rollout]
min_cooldown_seconds: 3600
prompt: |
Check project blockers, milestone progress, and generate the weekly risk summary for leadership.
extraction_schema:
Expand Down
3 changes: 3 additions & 0 deletions examples/ecommerce/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ watchers:
agent: ecommerce-ops
name: Customer activity tracker
schedule: 0 */6 * * *
notification_priority: normal
tags: [ecommerce, customer-ops]
min_cooldown_seconds: 300
prompt: |
Monitor customers for new orders, subscription changes, delivery requests, and support interactions.
extraction_schema:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Reaction for the `reconciliation-monitor` watcher.
*
* Persists any variance flagged during the daily 6am sweep as a durable
* `variance_flag` event tied to the affected account. Downstream agents
* (close-of-month rollup, audit prep) consume these events instead of
* re-extracting variances from the raw transaction stream.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface ReconciliationData {
variances?: Array<{
account: string;
amount: number;
direction: "over" | "under";
reason: string;
}>;
unreconciled_count?: number;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as ReconciliationData;
const variances = data.variances ?? [];
if (variances.length === 0) return;

for (const v of variances) {
await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: `Variance ${v.direction} on ${v.account}: ${v.amount} — ${v.reason}`,
semantic_type: "variance_flag",
metadata: {
account: v.account,
amount: v.amount,
direction: v.direction,
window_id: ctx.window.id,
unreconciled_count: data.unreconciled_count ?? null,
},
});
}
};
5 changes: 5 additions & 0 deletions examples/finance/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ watchers:
agent: finance
name: Reconciliation monitor
schedule: 0 6 * * 1-5
notification_priority: high
notification_channel: both
tags: [finance, reconciliation, daily]
min_cooldown_seconds: 3600
reaction_script: ./reactions/reconciliation-monitor.reaction.ts
prompt: |
Check accounts for unreconciled transactions, new variances, and approaching reporting deadlines. Lead with exceptions that need review.
extraction_schema:
Expand Down
4 changes: 4 additions & 0 deletions examples/leadership/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ watchers:
agent: leadership
name: Board action tracker
schedule: 0 8 * * *
notification_priority: high
notification_channel: both
tags: [leadership, daily, board]
agent_kind: notifier
prompt: |
Track board action items: check task delivery status, blocker resolution progress, and approaching deadlines for the next board packet.
extraction_schema:
Expand Down
7 changes: 7 additions & 0 deletions examples/legal/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ watchers:
agent: legal-review
name: Contract review tracker
schedule: 0 8 * * 1-5
notification_priority: high
tags: [legal, contract, daily]
min_cooldown_seconds: 1800
reactions_guidance: |
For any contract with `status: needs_counsel`, route an entity-scoped event
to the assigned reviewer. For contracts >90 days unsigned, escalate to the
counterparty owner; never auto-resolve risk items.
prompt: |
Review active contracts for approaching deadlines, unsigned agreements, and unresolved risk items. Flag any clauses that still need counsel approval.
extraction_schema:
Expand Down
51 changes: 51 additions & 0 deletions examples/lobu-crm/models/reactions/funnel-digest.reaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Reaction for the `funnel-digest` watcher.
*
* Runs after the weekly Monday-9am window completes. `ctx.extracted_data` is
* whatever the watcher's `extraction_schema` produced — funnel snapshot, top
* action, stale leads, etc. We persist the digest as a `funnel_digest` event
* linked to every lead the watcher knows about so the next digest can compare
* stage_counts week-over-week without re-running classification.
*
* Pair with `notification_priority: high` on the watcher — the OS notification
* fires regardless of whether this script succeeds; this just produces durable
* knowledge.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface DigestData {
top_action?: string;
stage_counts?: Record<string, number>;
conversations_this_week?: number;
gap?: string;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as DigestData;
const stageSummary = Object.entries(data.stage_counts ?? {})
.map(([stage, n]) => `${stage}: ${n}`)
.join(", ");
const content = [
`Weekly funnel digest — ${ctx.window.window_end.slice(0, 10)}`,
`Top action: ${data.top_action ?? "(none)"}`,
`Stages: ${stageSummary || "(empty)"}`,
`Conversations this week: ${data.conversations_this_week ?? 0}`,
data.gap ? `Gap: ${data.gap}` : null,
]
.filter(Boolean)
.join("\n");

await client.knowledge.save({
// Attaching to the whole watcher's entity set keeps the digest scoped to
// CRM data and discoverable from any lead the watcher already touches.
entity_ids: ctx.entities.map((e) => e.id),
content,
semantic_type: "funnel_digest",
metadata: {
window_id: ctx.window.id,
watcher_slug: ctx.watcher.slug,
stage_counts: data.stage_counts ?? {},
top_action: data.top_action ?? null,
},
});
};
44 changes: 44 additions & 0 deletions examples/lobu-crm/models/reactions/inbound-triage.reaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Reaction for the `inbound-triage` watcher.
*
* Fires every 2h after the watcher LLM extracts new and enriched leads from
* GitHub/X/HN signals. The script writes a `lead_interaction` event per
* recommended action so the next digest can count them — the watcher itself
* already creates the `lead` rows, so we don't duplicate that here.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface TriageData {
new_leads?: Array<{
name: string;
source: string;
stage: string;
why?: string;
}>;
recommended_actions?: string[];
notable?: boolean;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as TriageData;
// Nothing notable → nothing to persist. The watcher's prompt is explicit
// about not manufacturing noise; we mirror that here.
if (!data.notable) return;

const actions = data.recommended_actions ?? [];
if (actions.length === 0) return;

await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: [
`Triage run ${ctx.window.window_end.slice(0, 16)} — ${actions.length} action(s)`,
...actions.map((a, i) => `${i + 1}. ${a}`),
].join("\n"),
semantic_type: "lead_interaction",
metadata: {
window_id: ctx.window.id,
new_lead_count: data.new_leads?.length ?? 0,
action_count: actions.length,
},
});
};
9 changes: 9 additions & 0 deletions examples/lobu-crm/models/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ watchers:
agent: crm
name: Weekly funnel digest
schedule: 0 9 * * 1
notification_priority: high
notification_channel: both
tags: [crm, weekly]
min_cooldown_seconds: 3600
reaction_script: ./reactions/funnel-digest.reaction.ts
prompt: |
Produce the weekly funnel digest and post it to Slack. Keep it short.

Expand Down Expand Up @@ -173,6 +178,10 @@ watchers:
agent: crm
name: Inbound triage
schedule: 0 8-22/2 * * *
notification_priority: normal
tags: [crm, triage]
min_cooldown_seconds: 300
reaction_script: ./reactions/inbound-triage.reaction.ts
prompt: |
Look for new top-of-funnel signals since the last run, across the connectors
in this org:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Reaction for the `founder-activity-tracker` watcher.
*
* Records notable public activity (tweets, blog posts, hiring posts, fundraise
* rumors) as `founder_activity` events. The opportunity-matcher watcher reads
* these events to suggest cross-portfolio introductions.
*/
import type { ReactionContext } from "@lobu/connector-sdk";

interface FounderActivityData {
signals?: Array<{
founder: string;
activity_type: string;
summary: string;
importance?: "low" | "medium" | "high";
}>;
}

export default async (ctx: ReactionContext, client: any): Promise<void> => {
const data = ctx.extracted_data as FounderActivityData;
const signals = data.signals ?? [];
// High-importance only — low-noise channel for the intel feed.
const notable = signals.filter((s) => s.importance === "high");
if (notable.length === 0) return;

for (const s of notable) {
await client.knowledge.save({
entity_ids: ctx.entities.map((e) => e.id),
content: `${s.founder} — ${s.activity_type}: ${s.summary}`,
semantic_type: "founder_activity",
metadata: {
founder: s.founder,
activity_type: s.activity_type,
importance: s.importance,
window_id: ctx.window.id,
},
});
}
};
Loading
Loading