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:
David 2026-09-07 21:40:51 +02:00
parent 86564f1041
commit b83603461a
2 changed files with 193 additions and 0 deletions

View 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>;
}

View 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;