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