First running version

This commit is contained in:
qingjie.du
2026-03-30 17:39:13 +09:00
parent 5ffea3d849
commit bce2a5672c
67 changed files with 16503 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
/**
* 企画・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}をご用意しております。
ぜひお越しください。スタッフ一同、お待ちしております。
※ 本メールは予約システムより自動送信されています。`;
}