194 lines
5.6 KiB
TypeScript
194 lines
5.6 KiB
TypeScript
import { PROVIDER_CONFIG, getProviderApiKey, type AIProvider } from '@/stores/settings'
|
|
import { storage } from '@/services/storage'
|
|
|
|
export interface AIExtractedHU {
|
|
title: string
|
|
description: string
|
|
acceptance_criteria: string[]
|
|
priority?: string
|
|
story_points?: number
|
|
type?: 'feature' | 'bug' | 'task' | 'improvement'
|
|
}
|
|
|
|
export interface AIAnalysisResult {
|
|
hus: AIExtractedHU[]
|
|
summary: string
|
|
}
|
|
|
|
interface ActiveConfig {
|
|
baseUrl: string
|
|
apiKey: string
|
|
model: string
|
|
provider: AIProvider
|
|
}
|
|
|
|
function getActiveConfig(): ActiveConfig {
|
|
const raw = storage.getJSON<{ provider: AIProvider; modelId: string }>('alpha_settings')
|
|
if (raw) {
|
|
const provider = raw.provider || 'openrouter'
|
|
const model = raw.modelId || 'deepseek/deepseek-chat-v3-0324:free'
|
|
const baseUrl = PROVIDER_CONFIG[provider]?.baseUrl || ''
|
|
const apiKey = getProviderApiKey(provider)
|
|
return { baseUrl, apiKey, model, provider }
|
|
}
|
|
return {
|
|
baseUrl: PROVIDER_CONFIG.openrouter.baseUrl,
|
|
apiKey: getProviderApiKey('openrouter'),
|
|
model: 'deepseek/deepseek-chat-v3-0324:free',
|
|
provider: 'openrouter',
|
|
}
|
|
}
|
|
|
|
function buildHeaders(config: ActiveConfig): Record<string, string> {
|
|
const h: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
|
|
if (config.provider === 'openrouter') {
|
|
h['Authorization'] = `Bearer ${config.apiKey}`
|
|
h['HTTP-Referer'] = window.location.origin
|
|
h['X-Title'] = 'KAPPA Hub Alpha'
|
|
} else {
|
|
h['Authorization'] = `Bearer ${config.apiKey}`
|
|
h['Authorization'] = `Bearer ${config.apiKey}`
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: 'system' | 'user' | 'assistant'
|
|
content: string
|
|
}
|
|
|
|
export async function callAI(
|
|
messages: ChatMessage[],
|
|
temperature = 0.3,
|
|
maxTokens = 4096,
|
|
signal?: AbortSignal
|
|
): Promise<string> {
|
|
const config = getActiveConfig()
|
|
|
|
if (!config.apiKey) {
|
|
const label = PROVIDER_CONFIG[config.provider]?.label || config.provider
|
|
throw new Error(`No hay API key configurada para ${label}`)
|
|
}
|
|
if (!config.baseUrl) {
|
|
throw new Error(`El proveedor ${config.provider} no tiene API configurada`)
|
|
}
|
|
|
|
console.log(`[Alpha] AI call — provider: ${config.provider}, model: ${config.model}`)
|
|
|
|
const body: Record<string, unknown> = {
|
|
model: config.model,
|
|
messages,
|
|
temperature,
|
|
max_tokens: maxTokens,
|
|
}
|
|
|
|
const res = await fetch(config.baseUrl, {
|
|
method: 'POST',
|
|
headers: buildHeaders(config),
|
|
body: JSON.stringify(body),
|
|
signal,
|
|
})
|
|
|
|
const rawText = await res.text()
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`API error (${config.provider}): ${res.status} — ${rawText.slice(0, 300)}`)
|
|
}
|
|
|
|
const data = JSON.parse(rawText)
|
|
const content = data.choices?.[0]?.message?.content || null
|
|
|
|
if (!content) throw new Error('Respuesta vacía del proveedor de IA')
|
|
return stripThinkTags(content)
|
|
}
|
|
|
|
/**
|
|
* Elimina bloques de razonamiento interno del modelo (DeepSeek, etc.).
|
|
* Ej: "...." o "..."
|
|
*/
|
|
function stripThinkTags(text: string): string {
|
|
return text
|
|
.replace(/```thinking[\s\S]*?```/gi, '')
|
|
.replace(/```reasoning[\s\S]*?```/gi, '')
|
|
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
|
|
.replace(/^.*?\*\*Respuesta:/s, '')
|
|
.replace(/^.*?\*\*Final Answer:/s, '')
|
|
.trim()
|
|
}
|
|
|
|
export async function chatWithAI(
|
|
messages: { role: 'user' | 'assistant'; content: string }[],
|
|
systemPrompt?: string,
|
|
signal?: AbortSignal
|
|
): Promise<string> {
|
|
const msgs: ChatMessage[] = []
|
|
if (systemPrompt) {
|
|
msgs.push({ role: 'system', content: systemPrompt })
|
|
}
|
|
msgs.push(...messages)
|
|
return callAI(msgs, 0.7, 2048, signal)
|
|
}
|
|
|
|
const SYSTEM_PROMPT = `Eres un analista funcional experto en metodologías ágiles. Tu tarea es analizar transcripciones de reuniones y extraer Historias de Usuario (HUs) en formato estructurado.
|
|
|
|
Reglas:
|
|
1. Identifica cada requisito, funcionalidad, bug o mejora mencionada en la transcripción
|
|
2. Convierte cada uno en una HU con: título claro, descripción detallada, criterios de aceptación
|
|
3. Los criterios de aceptación deben ser verificables (condiciones específicas)
|
|
4. Usa el formato "Como [rol] quiero [funcionalidad] para [beneficio]" cuando sea posible
|
|
5. Asigna prioridad (Alta/Media/Baja) basada en urgencia implícita
|
|
6. No inventes información que no esté en la transcripción
|
|
7. Si el texto no contiene información relevante para HUs, devuelve un arreglo vacío
|
|
|
|
Responde SOLO con JSON válido en este formato:
|
|
{
|
|
"hus": [
|
|
{
|
|
"title": "Título de la HU",
|
|
"description": "Descripción detallada",
|
|
"acceptance_criteria": ["Criterio 1", "Criterio 2"],
|
|
"priority": "Alta|Media|Baja",
|
|
"story_points": 3,
|
|
"type": "feature|bug|task|improvement"
|
|
}
|
|
],
|
|
"summary": "Resumen breve del análisis (2-3 líneas)"
|
|
}`
|
|
|
|
export async function analyzeTranscription(
|
|
text: string,
|
|
projectName?: string,
|
|
signal?: AbortSignal
|
|
): Promise<AIAnalysisResult> {
|
|
const userContent = projectName
|
|
? `Proyecto: ${projectName}\n\nTranscripción:\n${text}`
|
|
: `Transcripción:\n${text}`
|
|
|
|
console.log(`[Alpha] AI analyze — text: ${text.length} chars`)
|
|
|
|
const content = await callAI(
|
|
[
|
|
{ role: 'system', content: SYSTEM_PROMPT },
|
|
{ role: 'user', content: userContent },
|
|
],
|
|
0.3,
|
|
4096,
|
|
signal,
|
|
)
|
|
|
|
try {
|
|
const jsonStr = content.replace(/```json\s*/gi, '').replace(/```\s*$/g, '').trim()
|
|
const result: AIAnalysisResult = JSON.parse(jsonStr)
|
|
console.log(`[Alpha] AI analysis complete — ${result.hus.length} HUs`)
|
|
return result
|
|
} catch (e) {
|
|
console.error('[Alpha] Failed to parse AI response:', content)
|
|
throw new Error('No se pudo parsear la respuesta de la IA')
|
|
}
|
|
}
|