feat(domain): politique de quota de l assistant commerce
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
This commit is contained in:
parent
86564f1041
commit
b83603461a
160
apps/backend/src/domain/ports/out/trade-assistant.port.ts
Normal file
160
apps/backend/src/domain/ports/out/trade-assistant.port.ts
Normal file
@ -0,0 +1,160 @@
|
||||
export const TRADE_AI = 'TRADE_AI';
|
||||
|
||||
export interface TradeAnswer {
|
||||
text: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
/** Capacites reellement invoquees pour produire cette reponse. */
|
||||
actions?: TradeAction[];
|
||||
}
|
||||
|
||||
/** Trace d'un appel d'outil, conservee avec le message et affichee a l'utilisateur. */
|
||||
export interface TradeAction {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outil propose au modele.
|
||||
*
|
||||
* Le domaine ne connait ni OpenAI ni MCP : il decrit un nom, une phrase et un
|
||||
* schema JSON. Chaque adaptateur traduit ensuite vers son propre format.
|
||||
*/
|
||||
export interface TradeToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute un outil au nom de l'utilisateur courant.
|
||||
*
|
||||
* La fonction est fournie par la couche application, deja liee a l'identite de
|
||||
* l'appelant : l'adaptateur ne peut pas choisir pour qui il agit.
|
||||
*/
|
||||
export type TradeToolInvoker = (
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
) => Promise<{ ok: boolean; result: unknown }>;
|
||||
|
||||
/** Un tour deja echange dans la conversation, envoye au modele comme contexte. */
|
||||
export interface TradeTurn {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** Un extrait du wiki retenu par la recherche, cite sous la reponse. */
|
||||
export interface TradePassage {
|
||||
id: string;
|
||||
/** Titre du sujet wiki, ex. « Procedures Douanieres ». */
|
||||
title: string;
|
||||
/** Section a l'interieur du sujet, ex. « Regimes Douaniers ». */
|
||||
section: string;
|
||||
/** Lien vers la page wiki, ex. `/dashboard/wiki/douanes`. */
|
||||
href: string;
|
||||
text: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface TradeAskInput {
|
||||
question: string;
|
||||
language: string;
|
||||
/** Tours precedents, du plus ancien au plus recent. */
|
||||
history: TradeTurn[];
|
||||
/** Extraits du wiki a citer en priorite. */
|
||||
passages: TradePassage[];
|
||||
/** Capacites ouvertes a cet utilisateur. Vide : l'assistant ne fait que repondre. */
|
||||
tools?: TradeToolDefinition[];
|
||||
invokeTool?: TradeToolInvoker;
|
||||
}
|
||||
|
||||
export interface TradeAiPort {
|
||||
isAvailable(): boolean;
|
||||
answer(input: TradeAskInput): Promise<TradeAnswer>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Recherche documentaire */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const TRADE_RETRIEVAL = 'TRADE_RETRIEVAL';
|
||||
|
||||
export interface TradeRetrievalPort {
|
||||
/** Extraits du wiki les plus proches de la question, dans sa langue. */
|
||||
search(question: string, language: string, limit?: number): Promise<TradePassage[]>;
|
||||
}
|
||||
|
||||
export const TRADE_EMBEDDINGS = 'TRADE_EMBEDDINGS';
|
||||
|
||||
export interface TradeEmbeddingPort {
|
||||
isAvailable(): boolean;
|
||||
/** Vecteurs normes, dans l'ordre des textes fournis. */
|
||||
embed(texts: string[]): Promise<number[][]>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Quota */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const TRADE_QUOTA = 'TRADE_QUOTA';
|
||||
|
||||
export interface TradeUsage {
|
||||
day: string;
|
||||
resetsAt: string;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export interface TradeQuotaPort {
|
||||
get(userId: string): Promise<TradeUsage>;
|
||||
reserve(userId: string, day: string, limit: number): Promise<boolean>;
|
||||
release(userId: string, day: string): Promise<void>;
|
||||
recordTokens(userId: string, day: string, answer: TradeAnswer): Promise<void>;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Conversations */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
export const TRADE_CONVERSATIONS = 'TRADE_CONVERSATIONS';
|
||||
|
||||
/** Source citee sous une reponse, telle qu'elle est persistee. */
|
||||
export interface TradeSource {
|
||||
title: string;
|
||||
section: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface TradeMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
sources: TradeSource[];
|
||||
/** Capacites invoquees pour produire ce message. Vide cote utilisateur. */
|
||||
actions: TradeAction[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface TradeConversationSummary {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
export interface TradeConversationRepository {
|
||||
list(userId: string): Promise<TradeConversationSummary[]>;
|
||||
create(userId: string, title: string): Promise<TradeConversationSummary>;
|
||||
/** `null` si la conversation n'existe pas ou n'appartient pas a l'utilisateur. */
|
||||
find(userId: string, conversationId: string): Promise<TradeConversationSummary | null>;
|
||||
messages(userId: string, conversationId: string): Promise<TradeMessage[]>;
|
||||
addMessage(
|
||||
conversationId: string,
|
||||
role: 'user' | 'assistant',
|
||||
content: string,
|
||||
sources?: TradeSource[],
|
||||
actions?: TradeAction[]
|
||||
): Promise<TradeMessage>;
|
||||
rename(userId: string, conversationId: string, title: string): Promise<void>;
|
||||
remove(userId: string, conversationId: string): Promise<void>;
|
||||
}
|
||||
33
apps/backend/src/domain/services/trade-assistant-policy.ts
Normal file
33
apps/backend/src/domain/services/trade-assistant-policy.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { SubscriptionPlanType } from '../value-objects/subscription-plan.vo';
|
||||
|
||||
/**
|
||||
* Questions par utilisateur et par jour.
|
||||
*
|
||||
* `-1` signifie illimite, comme partout ailleurs dans le domaine
|
||||
* (`maxLicenses`, `maxShipmentsPerYear`). Platinium est une offre sur devis :
|
||||
* elle n'est pas plafonnee.
|
||||
*/
|
||||
export const TRADE_DAILY_LIMITS: Readonly<Record<SubscriptionPlanType, number>> = {
|
||||
BRONZE: 3,
|
||||
SILVER: 10,
|
||||
GOLD: 15,
|
||||
PLATINIUM: -1,
|
||||
};
|
||||
|
||||
export const TRADE_SUPPORT_EMAIL = 'support@xpeditis.com';
|
||||
|
||||
/**
|
||||
* Limite d'une offre, avec repli sur Bronze.
|
||||
*
|
||||
* L'offre arrive d'une colonne de base de donnees : une valeur inconnue —
|
||||
* ancienne offre, ligne ecrite a la main — donnait `undefined`, puis un
|
||||
* `NaN` de bout en bout jusqu'a « NaN/undefined » dans l'interface. Le repli
|
||||
* sur l'offre la plus restrictive est le seul comportement sur.
|
||||
*/
|
||||
export function tradeDailyLimit(plan: string): number {
|
||||
return Object.prototype.hasOwnProperty.call(TRADE_DAILY_LIMITS, plan)
|
||||
? TRADE_DAILY_LIMITS[plan as SubscriptionPlanType]
|
||||
: TRADE_DAILY_LIMITS.BRONZE;
|
||||
}
|
||||
|
||||
export const isUnlimitedTradeQuota = (limit: number): boolean => limit < 0;
|
||||
Loading…
Reference in New Issue
Block a user