@@ -12,6 +12,7 @@ use tauri::{AppHandle, Emitter};
1212use crate :: error:: { AppError , Result } ;
1313use crate :: services:: XgenApiClient ;
1414use crate :: services:: xgen_api:: LlmProviderConfig ;
15+ use crate :: services:: tool_search;
1516
1617const MAX_TOOL_ROUNDS : usize = 5 ;
1718
@@ -81,10 +82,14 @@ impl LlmClient {
8182- 워크플로우 목록 조회, 생성, 실행, 삭제
8283- 스케줄 생성 및 관리
8384- 노드/도구/LLM 상태 확인
85+ - 문서 검색 및 RAG
8486- 사용자 질문에 친절하게 답변
8587
8688tool 호출 결과를 사용자에게 보기 좋게 정리해서 한국어로 답변하세요.
87- JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요."#
89+ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요.
90+
91+ 주어진 tool 중에서 사용자 요청에 가장 적합한 것을 선택하세요.
92+ 적합한 tool이 없으면 tool을 호출하지 말고 직접 답변하세요."#
8893 }
8994
9095 // ============================================================
@@ -575,7 +580,148 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요."#
575580 }
576581
577582 // ============================================================
578- // Unified entry point
583+ // Non-streaming API call (for tests, no AppHandle needed)
584+ // ============================================================
585+
586+ async fn call_anthropic_nostream (
587+ & self ,
588+ messages : & [ ChatMessage ] ,
589+ tools : & [ Value ] ,
590+ ) -> Result < Value > {
591+ let body = serde_json:: json!( {
592+ "model" : self . config. model,
593+ "max_tokens" : 4096 ,
594+ "system" : Self :: system_prompt( ) ,
595+ "messages" : messages,
596+ "tools" : tools,
597+ } ) ;
598+
599+ let resp = self . client
600+ . post ( "https://api.anthropic.com/v1/messages" )
601+ . header ( "x-api-key" , & self . config . api_key )
602+ . header ( "anthropic-version" , "2023-06-01" )
603+ . header ( "content-type" , "application/json" )
604+ . json ( & body)
605+ . send ( )
606+ . await
607+ . map_err ( |e| AppError :: LlmApi ( format ! ( "Request failed: {}" , e) ) ) ?;
608+
609+ let status = resp. status ( ) ;
610+ let text = resp. text ( ) . await . unwrap_or_default ( ) ;
611+ if !status. is_success ( ) {
612+ return Err ( AppError :: LlmApi ( format ! ( "HTTP {} - {}" , status. as_u16( ) , text) ) ) ;
613+ }
614+
615+ let response: Value = serde_json:: from_str ( & text)
616+ . map_err ( |e| AppError :: LlmApi ( format ! ( "Parse error: {}" , e) ) ) ?;
617+
618+ let stop_reason = response[ "stop_reason" ] . as_str ( ) . unwrap_or ( "end_turn" ) ;
619+ let content = response[ "content" ] . clone ( ) ;
620+
621+ Ok ( serde_json:: json!( { "role" : "assistant" , "content" : content, "stop_reason" : stop_reason} ) )
622+ }
623+
624+ /// Non-streaming tool use loop (for testing without AppHandle)
625+ pub async fn send_with_tools_nostream (
626+ & self ,
627+ messages : & mut Vec < ChatMessage > ,
628+ xgen_api : & XgenApiClient ,
629+ ) -> Result < String > {
630+ // 사용자 메시지에서 쿼리 추출하여 관련 tool 동적 검색
631+ let user_query = messages. last ( )
632+ . and_then ( |m| m. content . as_str ( ) )
633+ . unwrap_or ( "help" ) ;
634+
635+ let openapi_source = format ! ( "{}/api/openapi" , xgen_api. base_url( ) ) ;
636+ let tools = match tool_search:: search_tools_for_llm ( user_query, & openapi_source, Some ( 7 ) ) . await {
637+ Ok ( t) if !t. is_empty ( ) => {
638+ println ! ( " [tools] Found {} dynamic tools for '{}'" , t. len( ) , user_query) ;
639+ t
640+ }
641+ Ok ( _) | Err ( _) => {
642+ println ! ( " [tools] Fallback to hardcoded tools" ) ;
643+ XgenApiClient :: tool_definitions ( )
644+ }
645+ } ;
646+ let mut final_text = String :: new ( ) ;
647+
648+ for round in 0 ..MAX_TOOL_ROUNDS {
649+ println ! ( "[CLI test] round {}/{} ({})" , round + 1 , MAX_TOOL_ROUNDS , self . config. provider) ;
650+
651+ let response = self . call_anthropic_nostream ( messages, & tools) . await ?;
652+
653+ let stop_reason = response[ "stop_reason" ] . as_str ( ) . unwrap_or ( "end_turn" ) ;
654+ let content = response[ "content" ] . as_array ( )
655+ . ok_or_else ( || AppError :: LlmApi ( "Invalid response content" . into ( ) ) ) ?;
656+
657+ messages. push ( ChatMessage {
658+ role : "assistant" . into ( ) ,
659+ content : Value :: Array ( content. clone ( ) ) ,
660+ } ) ;
661+
662+ if stop_reason == "tool_use" {
663+ let mut tool_results: Vec < Value > = Vec :: new ( ) ;
664+
665+ for block in content {
666+ if block[ "type" ] . as_str ( ) == Some ( "tool_use" ) {
667+ let tool_id = block[ "id" ] . as_str ( ) . unwrap_or ( "" ) ;
668+ let tool_name = block[ "name" ] . as_str ( ) . unwrap_or ( "" ) ;
669+ let tool_input = block[ "input" ] . clone ( ) ;
670+
671+ println ! ( " [tool] {} → {:?}" , tool_name, tool_input) ;
672+
673+ // graph-tool-call call로 실행 (OpenAPI 기반 동적 실행)
674+ let result = match tool_search:: execute_tool_call (
675+ tool_name,
676+ & tool_input,
677+ & openapi_source,
678+ xgen_api. base_url ( ) ,
679+ xgen_api. auth_token ( ) ,
680+ ) . await {
681+ Ok ( v) => {
682+ let s = serde_json:: to_string_pretty ( & v) . unwrap_or_default ( ) ;
683+ println ! ( " [result] {}..." , s. chars( ) . take( 200 ) . collect:: <String >( ) ) ;
684+ s
685+ } ,
686+ Err ( e) => {
687+ // fallback: xgen_api.execute_tool
688+ println ! ( " [graph-tool-call fallback] {}" , e) ;
689+ match xgen_api. execute_tool ( tool_name, tool_input. clone ( ) ) . await {
690+ Ok ( v) => serde_json:: to_string_pretty ( & v) . unwrap_or_default ( ) ,
691+ Err ( e2) => format ! ( "Error: {}" , e2) ,
692+ }
693+ } ,
694+ } ;
695+
696+ tool_results. push ( serde_json:: json!( {
697+ "type" : "tool_result" ,
698+ "tool_use_id" : tool_id,
699+ "content" : result,
700+ } ) ) ;
701+ }
702+ }
703+
704+ messages. push ( ChatMessage {
705+ role : "user" . into ( ) ,
706+ content : Value :: Array ( tool_results) ,
707+ } ) ;
708+ } else {
709+ for block in content {
710+ if block[ "type" ] . as_str ( ) == Some ( "text" ) {
711+ if let Some ( text) = block[ "text" ] . as_str ( ) {
712+ final_text. push_str ( text) ;
713+ }
714+ }
715+ }
716+ break ;
717+ }
718+ }
719+
720+ Ok ( final_text)
721+ }
722+
723+ // ============================================================
724+ // Unified streaming entry point
579725 // ============================================================
580726
581727 async fn call_stream (
@@ -600,7 +746,22 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요."#
600746 session_id : & str ,
601747 app : & AppHandle ,
602748 ) -> Result < String > {
603- let tools = XgenApiClient :: tool_definitions ( ) ;
749+ // 동적 tool 검색
750+ let user_query = messages. last ( )
751+ . and_then ( |m| m. content . as_str ( ) )
752+ . unwrap_or ( "help" ) ;
753+
754+ let openapi_source = format ! ( "{}/api/openapi" , xgen_api. base_url( ) ) ;
755+ let tools = match tool_search:: search_tools_for_llm ( user_query, & openapi_source, Some ( 7 ) ) . await {
756+ Ok ( t) if !t. is_empty ( ) => {
757+ log:: info!( "Found {} dynamic tools for '{}'" , t. len( ) , user_query) ;
758+ t
759+ }
760+ Ok ( _) | Err ( _) => {
761+ log:: warn!( "Fallback to hardcoded tools" ) ;
762+ XgenApiClient :: tool_definitions ( )
763+ }
764+ } ;
604765 let mut final_text = String :: new ( ) ;
605766
606767 for round in 0 ..MAX_TOOL_ROUNDS {
@@ -634,9 +795,21 @@ JSON 결과는 핵심 정보만 추려서 읽기 쉽게 정리하세요."#
634795 data : serde_json:: json!( { "id" : tool_id, "name" : tool_name, "input" : tool_input} ) ,
635796 } ) ;
636797
637- let result = match xgen_api. execute_tool ( tool_name, tool_input) . await {
798+ let result = match tool_search:: execute_tool_call (
799+ tool_name,
800+ & tool_input,
801+ & openapi_source,
802+ xgen_api. base_url ( ) ,
803+ xgen_api. auth_token ( ) ,
804+ ) . await {
638805 Ok ( v) => serde_json:: to_string_pretty ( & v) . unwrap_or_default ( ) ,
639- Err ( e) => format ! ( "Error: {}" , e) ,
806+ Err ( e) => {
807+ log:: warn!( "graph-tool-call fallback: {}" , e) ;
808+ match xgen_api. execute_tool ( tool_name, tool_input) . await {
809+ Ok ( v) => serde_json:: to_string_pretty ( & v) . unwrap_or_default ( ) ,
810+ Err ( e2) => format ! ( "Error: {}" , e2) ,
811+ }
812+ }
640813 } ;
641814
642815 let _ = app. emit ( "cli:event" , CliStreamEvent {
0 commit comments