feat(ai): LLM 라우팅 + 에이전트/지식 UX 다수 개선
Build and Push Images / build-and-push (push) Has been cancelled
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user