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
2 changes: 0 additions & 2 deletions objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import * as dashboards from './src/dashboards/index.js';
import * as datasets from './src/datasets/index.js';
import * as reports from './src/reports/index.js';
import { allFlows } from './src/flows/index.js';
import { allAgents } from './src/agents/index.js';
import { allSkills } from './src/skills/index.js';
import * as profiles from './src/profiles/index.js';
import * as apps from './src/apps/index.js';
Expand Down Expand Up @@ -79,7 +78,6 @@ export default defineStack({
datasets: Object.values(datasets),
reports: Object.values(reports),
flows: allFlows,
agents: allAgents,
skills: allSkills,
permissions: Object.values(profiles),
apps: Object.values(apps),
Expand Down
18 changes: 18 additions & 0 deletions src/actions/lead.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export const ConvertLeadAction: Action = {
confirmText: 'Are you sure you want to convert this lead?',
successMessage: 'Lead converted successfully!',
refreshAfter: true,
// ADR-0011 — opt this Action in to AI, which materialises the
// `action_convert_lead` tool the `lead_qualification` skill references.
// Flow-typed with a target, so it has a headless path. `confirmText` makes
// the runtime treat it as approval-requiring, so an agent invocation lands
// in the HITL queue rather than converting a lead unattended — which is the
// behaviour we want for an irreversible outcome.
ai: {
exposed: true,
description:
'Converts a qualified lead into an account, contact and opportunity. Irreversible — requires human approval before it runs.',
},
};

/**
Expand Down Expand Up @@ -56,6 +67,13 @@ export const ScheduleFollowUpAction: Action = {
visible: P`record.is_converted == false && record.status != "unqualified" && record.status != "converted"`,
successMessage: 'Follow-up scheduled.',
refreshAfter: true,
// ADR-0011 — materialises `action_schedule_followup` for the
// `lead_qualification` skill. Additive and reversible, so no approval gate.
ai: {
exposed: true,
description:
'Schedules the next follow-up task on a lead, assigned to its owner, so the next touch lands on the rep list.',
},
};

/**
Expand Down
24 changes: 0 additions & 24 deletions src/agents/index.ts

This file was deleted.

41 changes: 0 additions & 41 deletions src/agents/sales-copilot.agent.ts

This file was deleted.

29 changes: 0 additions & 29 deletions src/agents/service-copilot.agent.ts

This file was deleted.

15 changes: 10 additions & 5 deletions src/docs/crm_admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,13 @@ revisits:
> on each record's detail page — they are intentionally **not** duplicated here.
> This guide documents only what those screens can't tell you.

## AI copilots

Two assistants ship with HotCRM: the **Sales Copilot** and the **Service
Copilot**. They operate over live CRM data and the actions exposed to them; the
end user simply asks the assistant — no agent selection required.
## AI skills

HotCRM ships **skills**, not assistants of its own. The platform provides the
assistant (ObjectStack ADR-0063: the runtime owns exactly two agents and the
surface you are in binds one); an app extends it by authoring skills that
attach by surface affinity. HotCRM's six — live data, lead qualification, case
triage, email drafting, revenue forecasting, customer 360 — operate over live
CRM data and reach anything that *changes* state through the same Actions the
UI buttons use, so permissions and audit are identical. The end user simply
asks — no agent selection required.
2 changes: 1 addition & 1 deletion src/objects/knowledge_article.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { F, P, cel } from '@objectstack/spec';
* Knowledge Article Object
*
* Reusable, search-indexed answers that back the Support Knowledge Base
* and ground the Service Copilot's "Suggest a resolution" skill.
* and ground the assistant's case-resolution skills.
*
* Lifecycle: draft → in_review → published → archived.
* Audience: public (customer portal visible) | internal (agent-only).
Expand Down
46 changes: 37 additions & 9 deletions src/skills/case-triage.skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,48 @@

import { defineSkill } from '@objectstack/spec';

/**
* Case Triage — prioritise a support case and route it to the right action.
*
* ADR-0109: this skill declares NO tool records. Triage itself is
* reasoning, not a tool — the agent reads the case with the platform's
* data tools and applies the rubric below.
*
* It deliberately calls NOTHING that mutates. Escalate and Close are
* `type: 'modal'` Actions: they collect a reason / resolution from a
* person, so they have no headless path and the runtime never
* materialises tools for them (ADR-0011). That is the right shape here —
* the agent supplies the judgement, the human supplies the words and the
* decision — so the skill recommends the button instead of pretending to
* press it.
*/
export const CaseTriageSkill = defineSkill({
name: 'case_triage',
label: 'Case Triage',
description: 'Triages a support case, assigns priority, and recommends the next action.',
description: 'Triages a support case, assigns a priority with its justification, and points at the escalate/close action.',

instructions: `When the user asks to triage, prioritise, or
classify a case:
1. Call triage_case with the current record.
2. If priority resolves to "critical", immediately recommend
escalation and draft an internal notification message.
3. Otherwise, propose a customer-facing response by handing off to
the response_drafting skill (do NOT inline-draft here).`,
instructions: `When the user asks to triage, prioritise, or classify a case:

tools: ['triage_case'],
1. Read the case first — call \`describe_object\` for \`crm_case\` (the
schema is alive; admins add fields), then \`get_record\` for the case
at hand. Never triage from memory of a previous turn.
2. Assign a priority yourself from what you read. There is no triage
tool and you do not need one — weigh, in order: customer tier and
contract value, whether the customer is blocked with no workaround,
how long the case has been open against its SLA, and the sentiment
of the latest customer message.
3. State the priority and the ONE reason that drove it. Cite the case
ID and the field values you used.
4. If the priority is critical, say so and point the user at
**Escalate Case** on the record header — offer a ready-to-paste
\`reason\` built from step 3. You cannot escalate yourself, and should
not imply otherwise.
5. If the case is resolved in substance, point at **Close Case** and
offer a \`resolution\` summary the user can paste.
6. For the customer-facing reply, hand off to the \`email_drafting\`
skill rather than drafting it here.`,

tools: ['describe_object', 'get_record'],

triggerPhrases: [
'triage this case',
Expand Down
37 changes: 28 additions & 9 deletions src/skills/email-drafting.skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,40 @@

import { defineSkill } from '@objectstack/spec';

/**
* Email Drafting — write the copy, then send it through the real Action.
*
* ADR-0109: writing a subject line and personalising a body is what the
* model does; it is not a tool call. Sending is HotCRM's `send_email`
* Action — `type: 'modal'`, so it collects the final copy from a person
* and has no headless path (ADR-0011). The agent therefore drafts and
* hands over; a human presses Send. For outbound email that review step
* is a feature, not a limitation.
*/
export const EmailDraftingSkill = defineSkill({
name: 'email_drafting',
label: 'Email Drafting',
description: 'Drafts personalised outbound emails and optimises subject lines for open rate.',
description: 'Drafts personalised outbound emails grounded in live contact data, ready for a human to review and send.',

instructions: `When the user asks to draft, write, or optimise an email:
1. Use generate_email_copy with the recipient and a brief intent.
2. Always run optimize_subject_line on the proposed subject before
returning the draft.
3. Personalise the body using personalize_content when the recipient
has known firmographics or recent activity.
4. Return the draft as { subject, body, alternatives } so the UI can
present A/B variants.`,

tools: ['generate_email_copy', 'optimize_subject_line', 'personalize_content', 'generate_email'],
1. Ground it in real data before writing a word: \`get_record\` the
contact (and the related account or opportunity when the request
references one) so names, titles and recent activity are accurate.
Use \`query_records\` when you need the recent history.
2. Write the copy yourself — subject and body. You do not have, and do
not need, a copy-generation tool. Aim for: a subject under 60
characters naming the concrete value, a first line referencing
something specific to THIS recipient rather than a generic
pleasantry, one clear ask, and no more than 150 words.
3. Offer two subject-line variants, say which you recommend and why,
so the user can A/B them.
4. Show the draft and stop. You cannot send — say so plainly rather
than implying a send is queued.
5. Point the user at **Send Email** on the contact record, where the
subject and body you drafted can be pasted, reviewed and sent.`,

tools: ['get_record', 'query_records'],

triggerPhrases: [
'draft an email',
Expand Down
45 changes: 33 additions & 12 deletions src/skills/lead-qualification.skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,48 @@
import { defineSkill } from '@objectstack/spec';

/**
* Lead Qualification Skill — qualifies inbound leads using BANT
* (Budget, Authority, Need, Timeline) and emits a 0–100 score.
* Lead Qualification — score an inbound lead with BANT, then act on it.
*
* ADR-0109: scoring is judgement, not a tool call. The lead is read
* with the platform's data tools; the two outcomes that change state
* (`convert_lead`, `schedule_followup`) are HotCRM's own Actions,
* reached through their materialised `action_<name>` tools.
*
* Auto-activates when the user is viewing a lead record so the LLM
* naturally surfaces the "Qualify Lead" capability without the user
* having to pick an agent.
* surfaces the capability without the user having to ask for it.
*/
export const LeadQualificationSkill = defineSkill({
name: 'lead_qualification',
label: 'Lead Qualification',
description: 'Qualifies inbound leads using BANT criteria and assigns a 0–100 score.',

instructions: `When the user asks to qualify, score, or analyze a lead:
1. Fetch the current lead with analyze_lead.
2. Apply BANT (Budget, Authority, Need, Timeline) reasoning.
3. Return a numeric score (0–100), a one-line justification per BANT
dimension, and the recommended next action.
4. If the score >= 70, propose calling suggest_next_action to draft
the follow-up.`,
instructions: `When the user asks to qualify, score, or analyse a lead:

1. Read the lead — \`describe_object\` for \`crm_lead\` first (fields
change), then \`get_record\`. Use \`query_records\` to pull the
related activity history when the lead has any.
2. Score it yourself, 0–100, with BANT. There is no scoring tool:
Budget (is spend confirmed or inferable from company size?),
Authority (is the contact a decision maker?), Need (is there a
stated problem you solve?), Timeline (is there a date?). Weight
them evenly unless the user says otherwise.
3. Report the score, one line of justification per BANT dimension
naming the field or activity you read, and the single recommended
next step. Cite the lead ID.
4. Score >= 70 and the user agrees → call \`action_convert_lead\`.
5. Otherwise, or when the user wants to nurture, call
\`action_schedule_followup\` with the date you recommend. Say why
that date.
6. Never convert without explicit confirmation — conversion is not
reversible from this surface.`,

tools: ['analyze_lead', 'suggest_next_action'],
tools: [
'describe_object',
'get_record',
'query_records',
'action_convert_lead',
'action_schedule_followup',
],

triggerPhrases: [
'qualify this lead',
Expand Down
6 changes: 3 additions & 3 deletions src/skills/live-data.skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { defineSkill } from '@objectstack/spec';
* Live Data — the foundation of HotCRM's "Wow #1" demo.
*
* Wires the platform's built-in metadata + data tools into the
* Sales Copilot so it always sees the current schema before
* querying. This is what makes "add a field, AI uses it 30s later"
* actually work — `describe_object` re-reads metadata on every call.
* assistant so it always sees the current schema before querying.
* This is what makes "add a field, AI uses it 30s later" actually
* work — `describe_object` re-reads metadata on every call.
*/
export const LiveDataSkill = defineSkill({
name: 'live_data',
Expand Down
Loading
Loading