Skip to content

Commit 6ff51f3

Browse files
SonAIengineclaude
andcommitted
feat: AI CLI gateway 모드 전환 — search_tools + call_tool meta-tool 아키텍처
- 기존 pre-search(translate_query → top-7 고정) → LLM 자율 검색/호출로 전환 - meta_tool_definitions(): search_tools + call_tool 2개 고정 tool 정의 - search_tools_text(): graph-tool-call search 실행 → 상세 파라미터 포함 텍스트 반환 - send_with_tools(): meta-tool dispatch (search_tools → 검색, call_tool → 실행) - system_prompt: gateway 모드 지시문 ("영문 검색 → call_tool 호출") - MAX_TOOL_ROUNDS: 5 → 10 (search+call = 최소 2라운드) - translate_query/to_llm_tool_schema/search_tools_for_llm 삭제 - cli.html: 🔍 검색 / ⚡ 호출 구분 표시 효과: LLM이 직접 검색 쿼리 생성 → "워크플로우 실행" 같은 요청도 정확히 API 매칭 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ca540b4 commit 6ff51f3

3 files changed

Lines changed: 271 additions & 259 deletions

File tree

src-cli/cli.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,14 @@
369369
}
370370
const tc = document.createElement('span');
371371
tc.className = 'tool-call';
372-
tc.textContent = `⚡ ${data.name}`;
372+
const name = data.name || '';
373+
if (name === 'search_tools') {
374+
tc.textContent = `🔍 ${data.input?.query || 'searching...'}`;
375+
} else if (name === 'call_tool') {
376+
tc.textContent = `⚡ ${data.input?.tool_name || 'calling...'}`;
377+
} else {
378+
tc.textContent = `⚡ ${name}`;
379+
}
373380
toolsEl.appendChild(tc);
374381
}
375382
break;

src-tauri/src/services/llm_client.rs

Lines changed: 72 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::services::XgenApiClient;
1414
use crate::services::xgen_api::LlmProviderConfig;
1515
use crate::services::tool_search;
1616

17-
const MAX_TOOL_ROUNDS: usize = 5;
17+
const MAX_TOOL_ROUNDS: usize = 10;
1818

1919
/// LLM API client — multi-provider support
2020
pub struct LlmClient {
@@ -76,20 +76,20 @@ impl LlmClient {
7676

7777
fn system_prompt() -> &'static str {
7878
r#"당신은 XGEN AI 플랫폼 어시스턴트입니다.
79-
사용자의 요청에 따라 XGEN API를 호출하여 워크플로우 관리, 실행, 모니터링 등을 수행합니다.
8079
81-
역할:
82-
- 워크플로우 목록 조회, 생성, 실행, 삭제
83-
- 스케줄 생성 및 관리
84-
- 노드/도구/LLM 상태 확인
85-
- 문서 검색 및 RAG
86-
- 사용자 질문에 친절하게 답변
87-
88-
tool 호출 결과를 사용자에게 보기 좋게 정리해서 한국어로 답변하세요.
89-
JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
90-
91-
주어진 tool 중에서 사용자 요청에 가장 적합한 것을 선택하세요.
92-
적합한 tool이 없으면 tool을 호출하지 말고 직접 답변하세요."#
80+
도구 사용 규칙:
81+
1. 사용자 요청을 처리하려면 먼저 search_tools로 관련 API를 검색하세요.
82+
- 검색 쿼리는 반드시 영문 키워드로 작성하세요 (예: "execute workflow", "list agents", "create schedule").
83+
- 한국어 요청이라도 영문으로 변환하여 검색하세요.
84+
- 검색 결과가 부족하면 다른 키워드로 다시 검색하세요.
85+
2. 검색 결과에서 적절한 tool을 선택하고, call_tool로 호출하세요.
86+
- tool_name은 검색 결과의 정확한 이름을 사용하세요.
87+
- arguments는 검색 결과의 파라미터 스키마에 맞게 구성하세요.
88+
3. 일반 질문이나 tool이 필요 없는 경우에는 직접 답변하세요.
89+
90+
응답 규칙:
91+
- API 결과는 핵심 정보만 추려서 한국어로 읽기 쉽게 정리하세요.
92+
- JSON을 그대로 보여주지 마세요."#
9393
}
9494

9595
// ============================================================
@@ -633,26 +633,14 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
633633
messages: &mut Vec<ChatMessage>,
634634
xgen_api: &XgenApiClient,
635635
) -> Result<String> {
636-
// 사용자 메시지에서 쿼리 추출하여 관련 tool 동적 검색
637-
let user_query = messages.last()
638-
.and_then(|m| m.content.as_str())
639-
.unwrap_or("help");
640-
641636
let openapi_source = format!("{}/api/openapi", xgen_api.base_url());
642-
let tools = match tool_search::search_tools_for_llm(user_query, &openapi_source, Some(7)).await {
643-
Ok(t) if !t.is_empty() => {
644-
println!(" [tools] Found {} dynamic tools for '{}'", t.len(), user_query);
645-
t
646-
}
647-
Ok(_) | Err(_) => {
648-
println!(" [tools] Fallback to hardcoded tools");
649-
XgenApiClient::tool_definitions()
650-
}
651-
};
637+
638+
// Gateway mode: 고정 meta-tool 2개
639+
let tools = tool_search::meta_tool_definitions();
652640
let mut final_text = String::new();
653641

654642
for round in 0..MAX_TOOL_ROUNDS {
655-
println!("[CLI test] round {}/{} ({})", round + 1, MAX_TOOL_ROUNDS, self.config.provider);
643+
println!("[CLI test] gateway round {}/{} ({})", round + 1, MAX_TOOL_ROUNDS, self.config.provider);
656644

657645
let response = self.call_anthropic_nostream(messages, &tools).await?;
658646

@@ -674,29 +662,36 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
674662
let tool_name = block["name"].as_str().unwrap_or("");
675663
let tool_input = block["input"].clone();
676664

677-
println!(" [tool] {} → {:?}", tool_name, tool_input);
678-
679-
// graph-tool-call call로 실행 (OpenAPI 기반 동적 실행)
680-
let result = match tool_search::execute_tool_call(
681-
tool_name,
682-
&tool_input,
683-
&openapi_source,
684-
xgen_api.base_url(),
685-
xgen_api.auth_token(),
686-
).await {
687-
Ok(v) => {
688-
let s = serde_json::to_string_pretty(&v).unwrap_or_default();
689-
println!(" [result] {}...", s.chars().take(200).collect::<String>());
690-
s
691-
},
692-
Err(e) => {
693-
// fallback: xgen_api.execute_tool
694-
println!(" [graph-tool-call fallback] {}", e);
695-
match xgen_api.execute_tool(tool_name, tool_input.clone()).await {
696-
Ok(v) => serde_json::to_string_pretty(&v).unwrap_or_default(),
697-
Err(e2) => format!("Error: {}", e2),
665+
println!(" [gateway] {} → {:?}", tool_name, tool_input);
666+
667+
let result = match tool_name {
668+
"search_tools" => {
669+
let query = tool_input["query"].as_str().unwrap_or("help");
670+
let top_k = tool_input["top_k"].as_u64().map(|v| v as usize);
671+
match tool_search::search_tools_text(query, &openapi_source, top_k).await {
672+
Ok(text) => {
673+
println!(" [search] {}...", text.chars().take(200).collect::<String>());
674+
text
675+
}
676+
Err(e) => format!("Search error: {}", e),
698677
}
699-
},
678+
}
679+
"call_tool" => {
680+
let actual_tool = tool_input["tool_name"].as_str().unwrap_or("");
681+
let args = tool_input.get("arguments").cloned().unwrap_or(serde_json::json!({}));
682+
match tool_search::execute_tool_call(
683+
actual_tool, &args, &openapi_source,
684+
xgen_api.base_url(), xgen_api.auth_token(),
685+
).await {
686+
Ok(v) => {
687+
let s = serde_json::to_string_pretty(&v).unwrap_or_default();
688+
println!(" [call] {}...", s.chars().take(200).collect::<String>());
689+
s
690+
}
691+
Err(e) => format!("Call error: {}", e),
692+
}
693+
}
694+
_ => format!("Unknown tool: {}", tool_name),
700695
};
701696

702697
tool_results.push(serde_json::json!({
@@ -752,26 +747,14 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
752747
session_id: &str,
753748
app: &AppHandle,
754749
) -> Result<String> {
755-
// 동적 tool 검색
756-
let user_query = messages.last()
757-
.and_then(|m| m.content.as_str())
758-
.unwrap_or("help");
759-
760750
let openapi_source = format!("{}/api/openapi", xgen_api.base_url());
761-
let tools = match tool_search::search_tools_for_llm(user_query, &openapi_source, Some(7)).await {
762-
Ok(t) if !t.is_empty() => {
763-
log::info!("Found {} dynamic tools for '{}'", t.len(), user_query);
764-
t
765-
}
766-
Ok(_) | Err(_) => {
767-
log::warn!("Fallback to hardcoded tools");
768-
XgenApiClient::tool_definitions()
769-
}
770-
};
751+
752+
// Gateway mode: 고정 meta-tool 2개 (search_tools + call_tool)
753+
let tools = tool_search::meta_tool_definitions();
771754
let mut final_text = String::new();
772755

773756
for round in 0..MAX_TOOL_ROUNDS {
774-
log::info!("CLI [{}] tool use round {}/{}", self.config.provider, round + 1, MAX_TOOL_ROUNDS);
757+
log::info!("CLI [{}] gateway round {}/{}", self.config.provider, round + 1, MAX_TOOL_ROUNDS);
775758

776759
let response = self.call_stream(messages, &tools, session_id, app).await?;
777760

@@ -793,29 +776,36 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
793776
let tool_name = block["name"].as_str().unwrap_or("");
794777
let tool_input = block["input"].clone();
795778

796-
log::info!("Executing tool: {}", tool_name);
779+
log::info!("Gateway dispatch: {} (id: {})", tool_name, tool_id);
797780

798781
let _ = app.emit("cli:event", CliStreamEvent {
799782
session_id: session_id.to_string(),
800783
event_type: "tool_call".into(),
801784
data: serde_json::json!({"id":tool_id,"name":tool_name,"input":tool_input}),
802785
});
803786

804-
let result = match tool_search::execute_tool_call(
805-
tool_name,
806-
&tool_input,
807-
&openapi_source,
808-
xgen_api.base_url(),
809-
xgen_api.auth_token(),
810-
).await {
811-
Ok(v) => serde_json::to_string_pretty(&v).unwrap_or_default(),
812-
Err(e) => {
813-
log::warn!("graph-tool-call fallback: {}", e);
814-
match xgen_api.execute_tool(tool_name, tool_input).await {
787+
// Meta-tool dispatch
788+
let result = match tool_name {
789+
"search_tools" => {
790+
let query = tool_input["query"].as_str().unwrap_or("help");
791+
let top_k = tool_input["top_k"].as_u64().map(|v| v as usize);
792+
match tool_search::search_tools_text(query, &openapi_source, top_k).await {
793+
Ok(text) => text,
794+
Err(e) => format!("Search error: {}", e),
795+
}
796+
}
797+
"call_tool" => {
798+
let actual_tool = tool_input["tool_name"].as_str().unwrap_or("");
799+
let args = tool_input.get("arguments").cloned().unwrap_or(serde_json::json!({}));
800+
match tool_search::execute_tool_call(
801+
actual_tool, &args, &openapi_source,
802+
xgen_api.base_url(), xgen_api.auth_token(),
803+
).await {
815804
Ok(v) => serde_json::to_string_pretty(&v).unwrap_or_default(),
816-
Err(e2) => format!("Error: {}", e2),
805+
Err(e) => format!("Call error: {}", e),
817806
}
818807
}
808+
_ => format!("Unknown tool: {}", tool_name),
819809
};
820810

821811
let _ = app.emit("cli:event", CliStreamEvent {
@@ -832,17 +822,15 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
832822
}
833823
}
834824

835-
// Validate: every tool_use must have a matching tool_result
825+
// Safety: ensure all tool_use IDs have matching tool_results
836826
let tool_use_ids: Vec<String> = content.iter()
837827
.filter(|b| b["type"].as_str() == Some("tool_use"))
838828
.filter_map(|b| b["id"].as_str().map(|s| s.to_string()))
839829
.collect();
840830
let tool_result_ids: Vec<String> = tool_results.iter()
841831
.filter_map(|r| r["tool_use_id"].as_str().map(|s| s.to_string()))
842832
.collect();
843-
log::info!("tool_use IDs: {:?}, tool_result IDs: {:?}", tool_use_ids, tool_result_ids);
844833

845-
// Safety: add placeholder tool_result for any missing IDs
846834
for use_id in &tool_use_ids {
847835
if !tool_result_ids.contains(use_id) {
848836
log::warn!("Missing tool_result for tool_use_id: {}", use_id);

0 commit comments

Comments
 (0)