feat(ai): LLM 라우팅 + 에이전트/지식 UX 다수 개선
Build and Push Images / build-and-push (push) Has been cancelled

LLM 호출
- llmClient.resolveProvider 우선순위 보강: model_name 정확 매칭(case-insensitive)을 1순위로 추가
- MODEL_PROVIDER_MAP regex 모두 /i 플래그 (대문자 'Qwen' 등 대응)
- max_tokens fallback 4096 → 16384
- API key 빈 값일 때 Authorization 헤더 자체를 생략 (인증 안 받는 로컬 LLM 대응)

multiAgentExecutionEngine
- 강제 max_tokens=2000 제거 → provider DB 값 fallback 으로 위임

에이전트 관리 (agents/page.tsx)
- 모델 드롭다운: 하드코딩 MODEL_GROUPS 제거 → 등록된 ai_llm_providers 동적 로드
- 같은 provider type(anthropic/openai/ollama 등) 끼리 그룹핑, 0개일 때 안내
- model_name 빈 값 row 는 dropdown 에서 자동 제외 (Radix 제약 회피)
- max_tokens 기본 16384, 빠른선택 버튼 (4K/16K/32K/64K) 추가
- 적용된 지식 파일 row 클릭 시 상세 모달 (내용/복사) 오픈
- hard delete 서비스 변경에 맞춘 UX

LLM 프로바이더 (providers/page.tsx)
- Ollama (로컬) 선택 시 'API 호출 URL' 입력 필드 노출
- '모델 ID' 직접 입력 필드 추가 (Qwen3.6-35B-A3B 같은 커스텀 모델)
- max_tokens / temperature 편집 UI 추가, 기본값 16384/0.7
- 더미 'ollama' API key 자동주입 제거 (실제 인증 토큰 보존)
- ollama 일 때 model_name 빈 값 저장 차단

대화 모니터링 (conversations/page.tsx)
- 메시지 안의 fenced code block (```lang ... ```) 자동 파싱
- 코드블록 별 다운로드 / 복사 버튼 (파일명 자동 추출 또는 snippet-{n}.{ext})
- 17종 언어 → 확장자 매핑 (html/jsp/ts/tsx/py/java/c/cpp/cs/go/rs/rb/php/sql/yml ...)
- 모달 폭 max-w-2xl → max-w-4xl

API 키 관리 (api-keys-manage/page.tsx)
- 그룹 실행 axios 호출 timeout 30s → 5분 (multi-agent 추론 시간 대응)

지식 라이브러리 (knowledge/page.tsx)
- TS2774 (Buffer.byteLength 함수 참조 truthy 체크) 컴파일 에러 수정 → 청크 빌드 정상화

aiAgentService
- delete() 진짜 hard DELETE 로 변경 + 외래키 의존 row(group_members/conversations/logs/usage) 정리
- list() 기본 필터에 status <> 'archived' 추가 → soft-deleted row 자동 숨김

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-28 18:41:52 +09:00
parent e4bca14a90
commit b4dc9b1927
8 changed files with 397 additions and 79 deletions
+15 -4
View File
@@ -11,6 +11,9 @@ export class AiAgentService {
if (filters?.status) {
sql += ` AND status = $${idx++}`;
params.push(filters.status);
} else {
// 기본: archived 제외 (소프트 삭제된 것은 목록에서 숨김)
sql += ` AND COALESCE(status, '') <> 'archived'`;
}
if (filters?.company_code) {
sql += ` AND (company_code = $${idx++} OR company_code IS NULL)`;
@@ -88,10 +91,18 @@ export class AiAgentService {
}
static async delete(id: number): Promise<boolean> {
const result = await query(
"UPDATE ai_agents SET status = 'archived', updated_at = NOW() WHERE id = $1",
[id]
);
// 1) 관련 row 정리 (외래키 위반 방지) — 그룹 멤버, 대화, 사용량/감사 로그 등
await query("DELETE FROM ai_agent_group_members WHERE agent_id = $1", [id]).catch(() => undefined);
await query("DELETE FROM ai_agent_conversations WHERE agent_id = $1", [id]).catch(() => undefined);
await query("DELETE FROM ai_analysis_logs WHERE agent_id = $1", [id]).catch(() => undefined);
await query("DELETE FROM ai_agent_usage WHERE agent_id = $1", [id]).catch(() => undefined);
// 2) 본 row hard delete
try {
await query("DELETE FROM ai_agents WHERE id = $1", [id]);
} catch (err) {
// 추가 외래키가 있어 실패하면 fallback 으로 archived
await query("UPDATE ai_agents SET status='archived', updated_at=NOW() WHERE id = $1", [id]);
}
return true;
}
+23 -12
View File
@@ -60,13 +60,13 @@ const DEFAULT_ENDPOINTS: Record<string, string> = {
ollama: "http://localhost:11434",
};
// 모델명 → 프로바이더 매핑 (프리픽스)
// 모델명 → 프로바이더 매핑 (프리픽스, 대소문자 무시)
const MODEL_PROVIDER_MAP: Array<[RegExp, string]> = [
[/^claude-/, "anthropic"],
[/^gpt-|^o[1-9]|^o3/, "openai"],
[/^gemini-/, "google"],
[/^deepseek-/, "deepseek"],
[/^llama|^mistral|^codellama|^phi|^qwen/, "ollama"],
[/^claude-/i, "anthropic"],
[/^gpt-|^o[1-9]|^o3/i, "openai"],
[/^gemini-/i, "google"],
[/^deepseek-/i, "deepseek"],
[/^llama|^mistral|^codellama|^phi|^qwen/i, "ollama"],
];
// ── 메인 클라이언트 ──────────────────────────────
@@ -92,8 +92,18 @@ export class LlmClient {
provider = rows[0];
}
// 1순위: 등록된 model_name 정확 매칭 (case-insensitive)
// 사용자가 "Qwen3.6-35B-A3B" 같은 커스텀 모델명을 prefix regex 가 못 잡는 케이스 대응
if (!provider && model) {
const rows = await query<AiLlmProvider>(
"SELECT * FROM ai_llm_providers WHERE LOWER(model_name) = LOWER($1) AND is_active = true ORDER BY priority LIMIT 1",
[model]
);
provider = rows[0];
}
// 2순위: 모델명 prefix 로 provider name 추론
if (!provider) {
// 모델명으로 프로바이더 이름 추론
let providerName: string | null = null;
for (const [re, name] of MODEL_PROVIDER_MAP) {
if (re.test(model)) { providerName = name; break; }
@@ -195,7 +205,7 @@ export class LlmClient {
model,
system: systemMsg?.content || undefined,
messages: nonSystemMsgs.map((m) => ({ role: m.role, content: m.content })),
max_tokens: params.max_tokens || provider.max_tokens || 4096,
max_tokens: params.max_tokens || provider.max_tokens || 16384,
temperature: params.temperature ?? provider.temperature ?? 0.7,
},
{
@@ -248,7 +258,7 @@ export class LlmClient {
model,
system: systemMsg?.content || undefined,
messages: nonSystemMsgs.map((m) => ({ role: m.role, content: m.content })),
max_tokens: params.max_tokens || provider.max_tokens || 4096,
max_tokens: params.max_tokens || provider.max_tokens || 16384,
temperature: params.temperature ?? provider.temperature ?? 0.7,
stream: true,
},
@@ -342,13 +352,14 @@ export class LlmClient {
{
model,
messages: params.messages,
max_tokens: params.max_tokens || provider.max_tokens || 4096,
max_tokens: params.max_tokens || provider.max_tokens || 16384,
temperature: params.temperature ?? provider.temperature ?? 0.7,
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
// 빈 키이면 Authorization 헤더 생략 (인증 안 받는 로컬 Ollama 등)
...(apiKey && apiKey.trim() ? { Authorization: `Bearer ${apiKey}` } : {}),
},
timeout: 120000,
}
@@ -370,7 +381,7 @@ export class LlmClient {
{
model,
messages: params.messages,
max_tokens: params.max_tokens || provider.max_tokens || 4096,
max_tokens: params.max_tokens || provider.max_tokens || 16384,
temperature: params.temperature ?? provider.temperature ?? 0.7,
stream: true,
},
@@ -313,7 +313,8 @@ export class MultiAgentExecutionEngine {
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
],
max_tokens: agent.config?.max_tokens || 2000,
// agent 별 명시값 > provider DB max_tokens > 16384 (LlmClient 내부 fallback 으로 위임)
max_tokens: agent.config?.max_tokens,
temperature: agent.config?.temperature || 0.7,
});