forked from Chatbot/v3-api
Merge pull request 'main' (#2) from s.hajizadeh/v3-api:main into main
Reviewed-on: Chatbot/v3-api#2
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Sender } from '../common/types/sender.type';
|
||||
import {
|
||||
buildBotMessage,
|
||||
isEscalationOffered,
|
||||
normalizeSuggestedQuestions,
|
||||
resolveAskText,
|
||||
resolveSessionId,
|
||||
toAskCompatiblePayload,
|
||||
@@ -37,6 +39,7 @@ describe('ai-v2 ask mapper', () => {
|
||||
messageId: '64f1a2b3c4d5e6f7a8b9c0d2',
|
||||
runId: 'bea53a8d-3745-420f-a50d-fb700de5611d',
|
||||
status: 'escalation_offered',
|
||||
suggested_questions: ['الف', ' الف ', '', null, 'ب'] as any,
|
||||
escalation: { summary: 'not in docs', handoff_context: {} },
|
||||
isNewSession: true,
|
||||
});
|
||||
@@ -44,5 +47,29 @@ describe('ai-v2 ask mapper', () => {
|
||||
expect(payload.messageId).toBe('64f1a2b3c4d5e6f7a8b9c0d2');
|
||||
expect(payload.runId).toBe('bea53a8d-3745-420f-a50d-fb700de5611d');
|
||||
expect(payload.offerOnlineChat).toBe(true);
|
||||
expect(payload.suggested_questions).toEqual(['الف', 'ب']);
|
||||
});
|
||||
|
||||
it('normalizes missing suggested_questions to an empty array', () => {
|
||||
expect(normalizeSuggestedQuestions(undefined)).toEqual([]);
|
||||
expect(normalizeSuggestedQuestions(null)).toEqual([]);
|
||||
expect(normalizeSuggestedQuestions([])).toEqual([]);
|
||||
expect(normalizeSuggestedQuestions('nope')).toEqual([]);
|
||||
});
|
||||
|
||||
it('stores suggested_questions on the bot message only when non-empty', () => {
|
||||
const withQuestions = buildBotMessage({
|
||||
text: 'پاسخ',
|
||||
now: Date.now() / 1000,
|
||||
suggested_questions: [' الف ', 'الف', 'ب'],
|
||||
});
|
||||
expect(withQuestions.suggested_questions).toEqual(['الف', 'ب']);
|
||||
|
||||
const without = buildBotMessage({
|
||||
text: 'پاسخ',
|
||||
now: Date.now() / 1000,
|
||||
suggested_questions: [],
|
||||
});
|
||||
expect(without.suggested_questions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AskCompatiblePayload = {
|
||||
runId: string | null;
|
||||
status: 'answered' | 'escalation_offered' | 'ai_unavailable';
|
||||
offerOnlineChat: boolean;
|
||||
suggested_questions: string[];
|
||||
escalation: AiV2RunResult['escalation'];
|
||||
};
|
||||
|
||||
@@ -33,6 +34,20 @@ export function resolveSessionId(sessionId?: string): string | undefined {
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export function normalizeSuggestedQuestions(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item !== 'string') continue;
|
||||
const text = item.trim();
|
||||
if (!text || seen.has(text)) continue;
|
||||
seen.add(text);
|
||||
out.push(text);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function isEscalationOffered(result: Pick<AiV2RunResult, 'status'>): boolean {
|
||||
return result.status === 'escalation_offered';
|
||||
}
|
||||
@@ -53,8 +68,12 @@ export function buildBotMessage(params: {
|
||||
now: number;
|
||||
runId?: string | null;
|
||||
status?: AskCompatiblePayload['status'];
|
||||
suggested_questions?: string[];
|
||||
escalation?: AiV2RunResult['escalation'];
|
||||
}) {
|
||||
const suggested_questions = normalizeSuggestedQuestions(
|
||||
params.suggested_questions,
|
||||
);
|
||||
return {
|
||||
messageId: new Types.ObjectId(),
|
||||
text: params.text,
|
||||
@@ -64,6 +83,8 @@ export function buildBotMessage(params: {
|
||||
createdAt: TimeHelper.unix2PersianTimeAndDate(params.now),
|
||||
createdISO: Date.now(),
|
||||
aiStatus: params.status,
|
||||
suggested_questions:
|
||||
suggested_questions.length > 0 ? suggested_questions : undefined,
|
||||
escalation: params.escalation || undefined,
|
||||
};
|
||||
}
|
||||
@@ -80,6 +101,7 @@ export function toAskCompatiblePayload(params: {
|
||||
messageId: string;
|
||||
runId?: string | null;
|
||||
status: AskCompatiblePayload['status'];
|
||||
suggested_questions?: unknown;
|
||||
escalation?: AiV2RunResult['escalation'];
|
||||
isNewSession: boolean;
|
||||
}): AskCompatiblePayload {
|
||||
@@ -100,6 +122,7 @@ export function toAskCompatiblePayload(params: {
|
||||
runId: params.runId ?? null,
|
||||
status: params.status,
|
||||
offerOnlineChat,
|
||||
suggested_questions: normalizeSuggestedQuestions(params.suggested_questions),
|
||||
escalation: params.escalation ?? null,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { Request, Response } from 'express';
|
||||
import { CurrentIdentity } from 'src/common/decorators/Identity.decorator';
|
||||
import { AiV2Service } from './ai-v2.service';
|
||||
import { CreateFeedbackDto, CreateRunDto } from './dto/threads.dto';
|
||||
import { AskRunDataDto, CreateFeedbackDto, CreateRunDto } from './dto/threads.dto';
|
||||
|
||||
@ApiTags('v2 AI')
|
||||
@ApiBearerAuth()
|
||||
@@ -24,13 +24,16 @@ export class AiV2ThreadsController {
|
||||
description:
|
||||
'Body: `{ question, sessionId? }`. Empty/omitted sessionId starts a new session. ' +
|
||||
'We generate our own messageId and store the AI run_id on the bot message. ' +
|
||||
'When AI_V2_STREAM=true, emits SSE `meta` → `token*` → `result`.',
|
||||
'When AI_V2_STREAM=true, emits SSE `meta` → `token*` → `result`. ' +
|
||||
'`suggested_questions` is on the JSON/SSE result payload and on the stored Bot message.',
|
||||
})
|
||||
@ApiBody({ type: CreateRunDto })
|
||||
@ApiProduces('application/json', 'text/event-stream')
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Ask-compatible JSON, or SSE when AI_V2_STREAM=true.',
|
||||
description:
|
||||
'Ask-compatible JSON (`data` includes `suggested_questions`), or SSE when AI_V2_STREAM=true. In stream mode the same object is the `event: result` data (after tokens).',
|
||||
type: AskRunDataDto,
|
||||
})
|
||||
async createRun(
|
||||
@Body() body: CreateRunDto,
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AiV2RunResult = {
|
||||
run_id: string;
|
||||
status: 'answered' | 'escalation_offered';
|
||||
message: string;
|
||||
suggested_questions?: string[] | null;
|
||||
escalation: { summary?: string; handoff_context?: Record<string, unknown> } | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export class AiV2Service {
|
||||
now,
|
||||
runId,
|
||||
status,
|
||||
suggested_questions: result?.suggested_questions,
|
||||
escalation: result?.escalation,
|
||||
});
|
||||
await this.session.updateOne(
|
||||
@@ -146,6 +147,7 @@ export class AiV2Service {
|
||||
messageId: String(botMessage.messageId),
|
||||
runId,
|
||||
status,
|
||||
suggested_questions: botMessage.suggested_questions,
|
||||
escalation: result?.escalation,
|
||||
isNewSession,
|
||||
});
|
||||
|
||||
@@ -57,3 +57,45 @@ export class CreateFeedbackDto {
|
||||
@MaxLength(2000)
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export class AskRunDataDto {
|
||||
@ApiProperty({ example: '64f1a2b3c4d5e6f7a8b9c0d1' })
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({ example: '1/20' })
|
||||
count: string;
|
||||
|
||||
@ApiProperty()
|
||||
question: string;
|
||||
|
||||
@ApiProperty({ description: 'Full assistant markdown. Always render this.' })
|
||||
answer: string;
|
||||
|
||||
@ApiProperty({ type: 'array', items: { type: 'object' } })
|
||||
history: unknown[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Our Mongo ObjectId for the new bot message. Use for feedback.',
|
||||
})
|
||||
messageId: string;
|
||||
|
||||
@ApiProperty({ nullable: true, description: 'Upstream AI run id. Ignore in UI.' })
|
||||
runId: string | null;
|
||||
|
||||
@ApiProperty({ enum: ['answered', 'escalation_offered', 'ai_unavailable'] })
|
||||
status: 'answered' | 'escalation_offered' | 'ai_unavailable';
|
||||
|
||||
@ApiProperty()
|
||||
offerOnlineChat: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
example: ['چگونه وارد پورتال درمانت شوم؟', 'چطور درخواست معرفینامه ثبت کنم؟'],
|
||||
description:
|
||||
'Clickable follow-up questions for this assistant turn. Always an array; may be empty. Also stored on the Bot message in history as suggested_questions.',
|
||||
})
|
||||
suggested_questions: string[];
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
escalation: { summary?: string; handoff_context?: Record<string, unknown> } | null;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export class SessionModel extends Document {
|
||||
react: { type: String, default: 'Nothing' },
|
||||
runId: { type: String, required: false },
|
||||
aiStatus: { type: String, required: false },
|
||||
suggested_questions: { type: [String], required: false, default: undefined },
|
||||
escalation: { type: Object, required: false },
|
||||
createdAt: { type: [String], required: true }, // [time, date]
|
||||
createdISO: { type: Date, required: true },
|
||||
@@ -104,6 +105,7 @@ export class SessionModel extends Document {
|
||||
edited: boolean;
|
||||
runId?: string;
|
||||
aiStatus?: string;
|
||||
suggested_questions?: string[];
|
||||
escalation?: {
|
||||
summary?: string;
|
||||
handoff_context?: Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user