109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
/**
|
|
* 企画・Plan Agent
|
|
*
|
|
* accessiblePools: [] (pool-less のみ)
|
|
* 利用可能ツール: query_crm, query_erp, query_hr (全て pool-less)
|
|
*
|
|
* タスク: 情報収集結果を受け取り、具体的な施策を立案する。
|
|
* Passkey (host-only) で店長に承認を求める。
|
|
*/
|
|
import type { A2ATaskRequest, A2ATaskResponse } from '../platform-types.js';
|
|
import { AgentBase } from '../agent-base.js';
|
|
|
|
interface AnalysisResult {
|
|
targetDate: string;
|
|
weather: { isRainy: boolean; precipitationProbability: number; summary: string };
|
|
inventory: { hasDessertStock: boolean; dessertsAvailable: Array<{ name: string; stock: number }> };
|
|
customers: { highCancelTendencyCount: number };
|
|
reservations: { tomorrowCount: number };
|
|
actionRecommended: boolean;
|
|
summary: string;
|
|
}
|
|
|
|
export class PlanningAgent extends AgentBase {
|
|
readonly agentId = 'planning-agent';
|
|
readonly accessiblePools: string[] = [];
|
|
|
|
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
|
|
const { analysis, rawData } = task.payload.parameters as {
|
|
analysis?: AnalysisResult;
|
|
rawData?: unknown;
|
|
};
|
|
|
|
if (!analysis) {
|
|
return this.error(task, 'No analysis data provided in task parameters');
|
|
}
|
|
|
|
if (!analysis.actionRecommended) {
|
|
return this.cancelled(task, `施策実施条件未達: ${analysis.summary}`);
|
|
}
|
|
|
|
try {
|
|
// 1. Get today's reservations from CRM for targeting
|
|
const reservationData = await this.callTool('query_crm', {
|
|
entity: 'reservations',
|
|
filter: `date=${analysis.targetDate}`,
|
|
}) as { reservations?: Array<{ customerName: string; customerId: string }> };
|
|
|
|
// 2. Get dessert inventory details from ERP
|
|
const inventoryData = await this.callTool('query_erp', {
|
|
entity: 'inventory',
|
|
item: 'dessert',
|
|
}) as { items?: Array<{ name: string; stock: number; costPerUnit: number }> };
|
|
|
|
// 3. Build the plan
|
|
const desserts = (inventoryData?.items ?? []).filter((i) => (i.stock ?? 0) > 0);
|
|
const targetDessert = desserts[0];
|
|
const targetCustomers = (reservationData?.reservations ?? []).slice(0, analysis.customers.highCancelTendencyCount);
|
|
|
|
const plan = {
|
|
title: '無料デザートキャンペーン',
|
|
targetDate: analysis.targetDate,
|
|
rationale: analysis.summary,
|
|
campaign: {
|
|
dessertItem: targetDessert?.name ?? 'デザート',
|
|
freeItemPerCustomer: 1,
|
|
totalCost: (targetDessert?.costPerUnit ?? 0) * targetCustomers.length,
|
|
},
|
|
targetCustomers: targetCustomers.map((c) => ({
|
|
customerId: c.customerId,
|
|
name: c.customerName,
|
|
})),
|
|
emailTemplate: buildEmailTemplate(analysis.targetDate, targetDessert?.name ?? 'デザート'),
|
|
createdAt: new Date().toISOString(),
|
|
status: 'pending_approval',
|
|
};
|
|
|
|
console.log(
|
|
`[planning] Plan created: ${plan.title} for ${targetCustomers.length} customers`,
|
|
);
|
|
|
|
// 4. Approval is handled by the Host (Passkey ceremony triggered by DispatchAgent)
|
|
// PlanningAgent returns the plan with status: pending_approval.
|
|
// The orchestration layer (Director → Dispatch) is responsible for auth flow.
|
|
return this.success(task, {
|
|
plan,
|
|
rawData,
|
|
nextAgent: 'dispatch-agent',
|
|
});
|
|
} catch (err) {
|
|
return this.error(task, (err as Error).message);
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildEmailTemplate(date: string, dessertName: string): string {
|
|
return `件名: 【特別ご招待】明日のご来店に無料${dessertName}をプレゼント
|
|
|
|
お客様へ
|
|
|
|
明日 ${date} のご予約、誠にありがとうございます。
|
|
|
|
明日は雨模様のお天気が予想されますが、
|
|
特別に無料の${dessertName}をご用意しております。
|
|
|
|
ぜひお越しください。スタッフ一同、お待ちしております。
|
|
|
|
※ 本メールは予約システムより自動送信されています。`;
|
|
}
|