컨텍스트¶
ADK(Agent Development Kit)에서 "컨텍스트(context)"는 에이전트와 그 도구들이 특정 작업을 수행하는 동안 사용할 수 있는 중요한 정보의 묶음을 의미합니다. 현재 작업이나 대화 차례를 효과적으로 처리하는 데 필요한 배경지식 및 리소스라고 생각할 수 있습니다.
에이전트는 종종 최신 사용자 메시지 이상의 정보가 있어야 제대로 작동합니다. 컨텍스트는 다음을 가능하게 하므로 필수적입니다.
- 상태 유지: 대화의 여러 단계에 걸쳐 세부 정보를 기억합니다(예: 사용자 선호도, 이전 계산 결과, 장바구니 항목). 이는 주로 세션 상태(session state)를 통해 관리됩니다.
- 데이터 전달: 한 단계(LLM 호출이나 도구 실행 등)에서 발견되거나 생성된 정보를 후속 단계와 공유합니다. 여기서도 세션 상태가 핵심적인 역할을 합니다.
- 서비스 접근: 다음과 같은 프레임워크 기능과 상호작용합니다.
- 아티팩트 저장소: 세션과 관련된 파일이나 데이터 덩어리(PDF, 이미지, 설정 파일 등)를 저장하거나 불러옵니다.
- 메모리: 사용자와 연결된 과거 상호작용이나 외부 지식 소스에서 관련 정보를 검색합니다.
- 인증: 도구가 외부 API에 안전하게 접근하는 데 필요한 자격 증명을 요청하고 검색합니다.
- ID 및 추적: 현재 어떤 에이전트가 실행 중인지(
agent.name) 알고, 로깅 및 디버깅을 위해 현재 요청-응답 주기(invocation_id)를 고유하게 식별합니다. - 도구별 작업: 현재 상호작용의 세부 정보에 접근해야 하는 인증 요청이나 메모리 검색과 같은 도구 내 특수 작업을 활성화합니다.
단일하고 완전한 사용자 요청-최종 응답 주기(호출(invocation))에 대한 이 모든 정보를 하나로 묶는 중심적인 요소는 InvocationContext입니다. 하지만 일반적으로 이 객체를 직접 생성하거나 관리하지는 않습니다. ADK 프레임워크는 호출이 시작될 때(예: runner.run_async를 통해) 이를 생성하고, 관련 컨텍스트 정보를 에이전트 코드, 콜백, 도구에 암묵적으로 전달합니다.
# 개념적 의사 코드: 프레임워크가 컨텍스트를 제공하는 방식 (내부 로직)
# runner = Runner(agent=my_root_agent, session_service=..., artifact_service=...)
# user_message = types.Content(...)
# session = session_service.get_session(...) # 또는 새로 생성
# --- runner.run_async(...) 내부 ---
# 1. 프레임워크가 이 특정 실행을 위한 주 컨텍스트를 생성합니다
# invocation_context = InvocationContext(
# invocation_id="this-run-에-대한-고유-id",
# session=session,
# user_content=user_message,
# agent=my_root_agent, # 시작 에이전트
# session_service=session_service,
# artifact_service=artifact_service,
# memory_service=memory_service,
# # ... 기타 필요한 필드 ...
# )
#
# 2. 프레임워크가 에이전트의 run 메서드를 호출하며 컨텍스트를 암묵적으로 전달합니다
# (에이전트의 메서드 시그니처가 이를 받게 됩니다, 예: runAsyncImpl(InvocationContext invocationContext))
# await my_root_agent.run_async(invocation_context)
# --- 내부 로직 끝 ---
#
# 개발자로서 여러분은 메서드 인자로 제공되는 컨텍스트 객체를 사용하게 됩니다.
/* 개념적 의사 코드: 프레임워크가 컨텍스트를 제공하는 방식 (내부 로직) */
sessionService := session.InMemoryService()
r, err := runner.New(runner.Config{
AppName: appName,
Agent: myAgent,
SessionService: sessionService,
})
if err != nil {
log.Fatalf("Failed to create runner: %v", err)
}
s, err := sessionService.Create(ctx, &session.CreateRequest{
AppName: appName,
UserID: userID,
})
if err != nil {
log.Fatalf("FATAL: Failed to create session: %v", err)
}
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("\nYou > ")
if !scanner.Scan() {
break
}
userInput := scanner.Text()
if strings.EqualFold(userInput, "quit") {
break
}
userMsg := genai.NewContentFromText(userInput, genai.RoleUser)
events := r.Run(ctx, s.Session.UserID(), s.Session.ID(), userMsg, agent.RunConfig{
StreamingMode: agent.StreamingModeNone,
})
fmt.Print("\nAgent > ")
for event, err := range events {
if err != nil {
log.Printf("ERROR during agent execution: %v", err)
break
}
fmt.Print(event.Content.Parts[0].Text)
}
}
/* 개념적 의사 코드: 프레임워크가 컨텍스트를 제공하는 방식 (내부 로직) */
InMemoryRunner runner = new InMemoryRunner(agent);
Session session = runner
.sessionService()
.createSession(runner.appName(), USER_ID, initialState, SESSION_ID )
.blockingGet();
try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)) {
while (true) {
System.out.print("\nYou > ");
}
String userInput = scanner.nextLine();
if ("quit".equalsIgnoreCase(userInput)) {
break;
}
Content userMsg = Content.fromParts(Part.fromText(userInput));
Flowable<Event> events = runner.runAsync(session.userId(), session.id(), userMsg);
System.out.print("\nAgent > ");
events.blockingForEach(event -> System.out.print(event.stringifyContent()));
}
다양한 종류의 컨텍스트¶
InvocationContext가 포괄적인 내부 컨테이너 역할을 하지만, ADK는 특정 상황에 맞춰진 특수한 컨텍스트 객체를 제공합니다. 이를 통해 모든 곳에서 내부 컨텍스트의 전체 복잡성을 다룰 필요 없이 당면한 작업에 적합한 도구와 권한을 가질 수 있습니다. 여러분이 마주하게 될 다양한 "종류"는 다음과 같습니다.
-
InvocationContext- 사용 위치: 에이전트의 핵심 구현 메서드(
_run_async_impl,_run_live_impl) 내에서ctx인자로 직접 받습니다. - 목적: 현재 호출의 전체 상태에 대한 접근을 제공합니다. 이는 가장 포괄적인 컨텍스트 객체입니다.
- 주요 내용:
session(state와events포함), 현재agent인스턴스,invocation_id, 초기user_content, 설정된 서비스(artifact_service,memory_service,session_service)에 대한 참조, 그리고 라이브/스트리밍 모드와 관련된 필드에 직접 접근할 수 있습니다. - 사용 사례: 주로 에이전트의 핵심 로직이 전체 세션이나 서비스에 직접 접근해야 할 때 사용됩니다. 하지만 종종 상태 및 아티팩트 상호작용은 자체 컨텍스트를 사용하는 콜백/도구에 위임됩니다. 또한 호출 자체를 제어하는 데 사용됩니다(예:
ctx.end_invocation = True설정).
# 의사 코드: InvocationContext를 받는 에이전트 구현 from google.adk.agents import BaseAgent from google.adk.agents.invocation_context import InvocationContext from google.adk.events import Event from typing import AsyncGenerator class MyAgent(BaseAgent): async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]: # 직접 접근 예제 agent_name = ctx.agent.name session_id = ctx.session.id print(f"에이전트 {agent_name}가 세션 {session_id}에서 호출 {ctx.invocation_id}을(를) 위해 실행 중") # ... ctx를 사용하는 에이전트 로직 ... yield # ... 이벤트 ...import ( "google.golang.org/adk/agent" "google.golang.org/adk/session" ) // Pseudocode: Agent implementation receiving InvocationContext type MyAgent struct { } func (a *MyAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { return func(yield func(*session.Event, error) bool) { // Direct access example agentName := ctx.Agent().Name() sessionID := ctx.Session().ID() fmt.Printf("Agent %s running in session %s for invocation %s\n", agentName, sessionID, ctx.InvocationID()) // ... agent logic using ctx ... yield(&session.Event{Author: agentName}, nil) } }// 의사 코드: InvocationContext를 받는 에이전트 구현 import com.google.adk.agents.BaseAgent; import com.google.adk.agents.InvocationContext; LlmAgent root_agent = LlmAgent.builder() .model("gemini-***") .name("sample_agent") .description("사용자 질문에 답합니다.") .instruction( """ 여기에 에이전트를 위한 지침을 제공하세요. """ ) .tools(sampleTool) .outputKey("YOUR_KEY") .build(); ConcurrentMap<String, Object> initialState = new ConcurrentHashMap<>(); initialState.put("YOUR_KEY", ""); InMemoryRunner runner = new InMemoryRunner(agent); Session session = runner .sessionService() .createSession(runner.appName(), USER_ID, initialState, SESSION_ID ) .blockingGet(); try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)) { while (true) { System.out.print("\nYou > "); String userInput = scanner.nextLine(); if ("quit".equalsIgnoreCase(userInput)) { break; } Content userMsg = Content.fromParts(Part.fromText(userInput)); Flowable<Event> events = runner.runAsync(session.userId(), session.id(), userMsg); System.out.print("\nAgent > "); events.blockingForEach(event -> System.out.print(event.stringifyContent())); } protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) { // 직접 접근 예제 String agentName = invocationContext.agent.name String sessionId = invocationContext.session.id String invocationId = invocationContext.invocationId System.out.println("에이전트 " + agent_name + "가 세션 " + session_id + "에서 호출 " + invocationId + "을(를) 위해 실행 중") // ... ctx를 사용하는 에이전트 로직 ... } - 사용 위치: 에이전트의 핵심 구현 메서드(
-
ReadonlyContext- 사용 위치: 기본 정보에 대한 읽기 접근만 필요하고 수정은 허용되지 않는 시나리오(예:
InstructionProvider함수)에서 제공됩니다. 다른 컨텍스트의 기본 클래스이기도 합니다. - 목적: 기본적인 컨텍스트 세부 정보에 대한 안전한 읽기 전용 뷰를 제공합니다.
- 주요 내용:
invocation_id,agent_name, 그리고 현재state의 읽기 전용 뷰.
# 의사 코드: ReadonlyContext를 받는 Instruction provider from google.adk.agents.readonly_context import ReadonlyContext def my_instruction_provider(context: ReadonlyContext) -> str: # 읽기 전용 접근 예제 user_tier = context.state().get("user_tier", "standard") # 상태를 읽을 수 있음 # context.state['new_key'] = 'value' # 이것은 일반적으로 오류를 발생시키거나 효과가 없음 return f"{user_tier} 사용자에 대한 요청을 처리하세요."import "google.golang.org/adk/agent" // Pseudocode: Instruction provider receiving ReadonlyContext func myInstructionProvider(ctx agent.ReadonlyContext) (string, error) { // Read-only access example userTier, err := ctx.ReadonlyState().Get("user_tier") if err != nil { userTier = "standard" // Default value } // ctx.ReadonlyState() has no Set method since State() is read-only. return fmt.Sprintf("Process the request for a %v user.", userTier), nil }// 의사 코드: ReadonlyContext를 받는 Instruction provider import com.google.adk.agents.ReadonlyContext; public String myInstructionProvider(ReadonlyContext context){ // 읽기 전용 접근 예제 String userTier = context.state().get("user_tier", "standard"); context.state().put('new_key', 'value'); //이것은 일반적으로 오류를 발생시킴 return userTier + " 사용자에 대한 요청을 처리하세요."; } - 사용 위치: 기본 정보에 대한 읽기 접근만 필요하고 수정은 허용되지 않는 시나리오(예:
-
CallbackContext- 사용 위치: 에이전트 생명주기 콜백(
before_agent_callback,after_agent_callback) 및 모델 상호작용 콜백(before_model_callback,after_model_callback)에callback_context로 전달됩니다. - 목적: 특히 콜백 내에서 상태를 검사 및 수정하고, 아티팩트와 상호작용하며, 호출 세부 정보에 접근하는 것을 용이하게 합니다.
- 주요 기능 (
ReadonlyContext에 추가됨):- 변경 가능한
state속성: 세션 상태를 읽고 쓸 수 있습니다. 여기서 변경된 사항(callback_context.state['key'] = value)은 추적되며 콜백 후 프레임워크가 생성하는 이벤트와 연결됩니다. - 아티팩트 메서드: 설정된
artifact_service와 상호작용하기 위한load_artifact(filename)및save_artifact(filename, part)메서드. - 직접적인
user_content접근.
- 변경 가능한
# 의사 코드: CallbackContext를 받는 콜백 from google.adk.agents.callback_context import CallbackContext from google.adk.models import LlmRequest from google.genai import types from typing import Optional def my_before_model_cb(callback_context: CallbackContext, request: LlmRequest) -> Optional[types.Content]: # 상태 읽기/쓰기 예제 call_count = callback_context.state.get("model_calls", 0) callback_context.state["model_calls"] = call_count + 1 # 상태 수정 # 선택적으로 아티팩트 로드 # config_part = callback_context.load_artifact("model_config.json") print(f"호출 {callback_context.invocation_id}에 대해 모델 호출 #{call_count + 1} 준비 중") return None # 모델 호출 진행 허용import ( "google.golang.org/adk/agent" "google.golang.org/adk/model" ) // Pseudocode: Callback receiving CallbackContext func myBeforeModelCb(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { // Read/Write state example callCount, err := ctx.State().Get("model_calls") if err != nil { callCount = 0 // Default value } newCount := callCount.(int) + 1 if err := ctx.State().Set("model_calls", newCount); err != nil { return nil, err } // Optionally load an artifact // configPart, err := ctx.Artifacts().Load("model_config.json") fmt.Printf("Preparing model call #%d for invocation %s\n", newCount, ctx.InvocationID()) return nil, nil // Allow model call to proceed }// 의사 코드: CallbackContext를 받는 콜백 import com.google.adk.agents.CallbackContext; import com.google.adk.models.LlmRequest; import com.google.genai.types.Content; import java.util.Optional; public Maybe<LlmResponse> myBeforeModelCb(CallbackContext callbackContext, LlmRequest request){ // 상태 읽기/쓰기 예제 callCount = callbackContext.state().get("model_calls", 0) callbackContext.state().put("model_calls") = callCount + 1 # 상태 수정 // 선택적으로 아티팩트 로드 // Maybe<Part> configPart = callbackContext.loadArtifact("model_config.json"); System.out.println("모델 호출 " + callCount + 1 + " 준비 중"); return Maybe.empty(); // 모델 호출 진행 허용 } - 사용 위치: 에이전트 생명주기 콜백(
-
ToolContext- 사용 위치:
FunctionTool을 지원하는 함수와 도구 실행 콜백(before_tool_callback,after_tool_callback)에tool_context로 전달됩니다. - 목적:
CallbackContext가 제공하는 모든 것과 더불어 인증 처리, 메모리 검색, 아티팩트 목록 조회와 같이 도구 실행에 필수적인 특수 메서드를 제공합니다. - 주요 기능 (
CallbackContext에 추가됨):- 인증 메서드: 인증 흐름을 트리거하는
request_credential(auth_config)와 사용자/시스템이 제공한 자격 증명을 검색하는get_auth_response(auth_config). - 아티팩트 목록 조회: 세션에서 사용 가능한 아티팩트를 찾는
list_artifacts(). - 메모리 검색: 설정된
memory_service에 질의하는search_memory(query). function_call_id속성: 이 도구 실행을 트리거한 LLM의 특정 함수 호출을 식별하며, 인증 요청이나 응답을 올바르게 연결하는 데 중요합니다.actions속성: 이 단계의EventActions객체에 직접 접근하여 도구가 상태 변경, 인증 요청 등을 신호할 수 있게 합니다.
- 인증 메서드: 인증 흐름을 트리거하는
# 의사 코드: ToolContext를 받는 도구 함수 from google.adk.tools import ToolContext from typing import Dict, Any # 이 함수가 FunctionTool에 의해 래핑되었다고 가정 def search_external_api(query: str, tool_context: ToolContext) -> Dict[str, Any]: api_key = tool_context.state.get("api_key") if not api_key: # 필요한 인증 설정 정의 # auth_config = AuthConfig(...) # tool_context.request_credential(auth_config) # 자격 증명 요청 # 'actions' 속성을 사용하여 인증 요청이 이루어졌음을 신호 # tool_context.actions.requested_auth_configs[tool_context.function_call_id] = auth_config return {"status": "인증 필요"} # API 키 사용... print(f"API 키를 사용하여 쿼리 '{query}'에 대해 도구 실행 중. 호출: {tool_context.invocation_id}") # 선택적으로 메모리 검색 또는 아티팩트 목록 조회 # relevant_docs = tool_context.search_memory(f"{query}와 관련된 정보") # available_files = tool_context.list_artifacts() return {"result": f"{query}에 대한 데이터 가져옴."}import "google.golang.org/adk/tool" // Pseudocode: Tool function receiving ToolContext type searchExternalAPIArgs struct { Query string `json:"query" jsonschema:"The query to search for."` } func searchExternalAPI(tc tool.Context, input searchExternalAPIArgs) (string, error) { apiKey, err := tc.State().Get("api_key") if err != nil || apiKey == "" { // In a real scenario, you would define and request credentials here. // This is a conceptual placeholder. return "", fmt.Errorf("auth required") } // Use the API key... fmt.Printf("Tool executing for query '%s' using API key. Invocation: %s\n", input.Query, tc.InvocationID()) // Optionally search memory or list artifacts // relevantDocs, _ := tc.SearchMemory(tc, "info related to %s", input.Query)) // availableFiles, _ := tc.Artifacts().List() return fmt.Sprintf("Data for %s fetched.", input.Query), nil }// 의사 코드: ToolContext를 받는 도구 함수 import com.google.adk.tools.ToolContext; import java.util.HashMap; import java.util.Map; // 이 함수가 FunctionTool에 의해 래핑되었다고 가정 public Map<String, Object> searchExternalApi(String query, ToolContext toolContext){ String apiKey = toolContext.state.get("api_key"); if(apiKey.isEmpty()){ // 필요한 인증 설정 정의 // authConfig = AuthConfig(...); // toolContext.requestCredential(authConfig); # 자격 증명 요청 // 'actions' 속성을 사용하여 인증 요청이 이루어졌음을 신호 ... return Map.of("status", "인증 필요"); // API 키 사용... System.out.println("API 키를 사용하여 쿼리 " + query + "에 대해 도구 실행 중."); // 선택적으로 아티팩트 목록 조회 // Single<List<String>> availableFiles = toolContext.listArtifacts(); return Map.of("result", query + "에 대한 데이터 가져옴"); } - 사용 위치:
이러한 다양한 컨텍스트 객체와 언제 사용해야 하는지를 이해하는 것은 ADK 애플리케이션의 상태를 효과적으로 관리하고, 서비스에 접근하며, 흐름을 제어하는 데 핵심적입니다. 다음 섹션에서는 이러한 컨텍스트를 사용하여 수행할 수 있는 일반적인 작업을 자세히 설명합니다.
컨텍스트를 사용한 일반적인 작업¶
이제 다양한 컨텍스트 객체를 이해했으니, 에이전트와 도구를 만들 때 이를 사용하여 일반적인 작업을 수행하는 방법에 초점을 맞춰 보겠습니다.
정보 접근¶
컨텍스트에 저장된 정보를 읽어야 할 경우가 자주 있습니다.
-
세션 상태 읽기: 이전 단계에서 저장된 데이터나 사용자/앱 수준 설정에 접근합니다.
state속성에 대해 딕셔너리처럼 접근합니다.# 의사 코드: 도구 함수 내에서 from google.adk.tools import ToolContext def my_tool(tool_context: ToolContext, **kwargs): user_pref = tool_context.state.get("user_display_preference", "default_mode") api_endpoint = tool_context.state.get("app:api_endpoint") # 앱 수준 상태 읽기 if user_pref == "dark_mode": # ... 다크 모드 로직 적용 ... pass print(f"사용 중인 API 엔드포인트: {api_endpoint}") # ... 나머지 도구 로직 ... # 의사 코드: 콜백 함수 내에서 from google.adk.agents.callback_context import CallbackContext def my_callback(callback_context: CallbackContext, **kwargs): last_tool_result = callback_context.state.get("temp:last_api_result") # 임시 상태 읽기 if last_tool_result: print(f"마지막 도구에서 임시 결과 발견: {last_tool_result}") # ... 콜백 로직 ...import ( "google.golang.org/adk/agent" "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/genai" ) // Pseudocode: In a Tool function type toolArgs struct { // Define tool-specific arguments here } type toolResults struct { // Define tool-specific results here } // Example tool function demonstrating state access func myTool(tc tool.Context, input toolArgs) (toolResults, error) { userPref, err := tc.State().Get("user_display_preference") if err != nil { userPref = "default_mode" } apiEndpoint, _ := tc.State().Get("app:api_endpoint") // Read app-level state if userPref == "dark_mode" { // ... apply dark mode logic ... } fmt.Printf("Using API endpoint: %v\n", apiEndpoint) // ... rest of tool logic ... return toolResults{}, nil } // Pseudocode: In a Callback function func myCallback(ctx agent.CallbackContext) (*genai.Content, error) { lastToolResult, err := ctx.State().Get("temp:last_api_result") // Read temporary state if err == nil { fmt.Printf("Found temporary result from last tool: %v\n", lastToolResult) } else { fmt.Println("No temporary result found.") } // ... callback logic ... return nil, nil }// 의사 코드: 도구 함수 내에서 import com.google.adk.tools.ToolContext; public void myTool(ToolContext toolContext){ String userPref = toolContext.state().get("user_display_preference"); String apiEndpoint = toolContext.state().get("app:api_endpoint"); // 앱 수준 상태 읽기 if(userPref.equals("dark_mode")){ // ... 다크 모드 로직 적용 ... pass } System.out.println("사용 중인 API 엔드포인트: " + api_endpoint); // ... 나머지 도구 로직 ... } // 의사 코드: 콜백 함수 내에서 import com.google.adk.agents.CallbackContext; public void myCallback(CallbackContext callbackContext){ String lastToolResult = (String) callbackContext.state().get("temp:last_api_result"); // 임시 상태 읽기 } if(!(lastToolResult.isEmpty())){ System.out.println("마지막 도구에서 임시 결과 발견: " + lastToolResult); } // ... 콜백 로직 ... -
현재 식별자 가져오기: 로깅이나 현재 작업을 기반으로 한 사용자 정의 로직에 유용합니다.
# 의사 코드: 모든 컨텍스트에서 (ToolContext 예시) from google.adk.tools import ToolContext def log_tool_usage(tool_context: ToolContext, **kwargs): agent_name = tool_context.agent_name inv_id = tool_context.invocation_id func_call_id = getattr(tool_context, 'function_call_id', 'N/A') # ToolContext에만 해당 print(f"로그: 호출={inv_id}, 에이전트={agent_name}, 함수호출ID={func_call_id} - 도구 실행됨.")import "google.golang.org/adk/tool" // Pseudocode: In any context (ToolContext shown) type logToolUsageArgs struct{} type logToolUsageResult struct { Status string `json:"status"` } func logToolUsage(tc tool.Context, args logToolUsageArgs) (logToolUsageResult, error) { agentName := tc.AgentName() invID := tc.InvocationID() funcCallID := tc.FunctionCallID() fmt.Printf("Log: Invocation=%s, Agent=%s, FunctionCallID=%s - Tool Executed.\n", invID, agentName, funcCallID) return logToolUsageResult{Status: "Logged successfully"}, nil }// 의사 코드: 모든 컨텍스트에서 (ToolContext 예시) import com.google.adk.tools.ToolContext; public void logToolUsage(ToolContext toolContext){ String agentName = toolContext.agentName; String invId = toolContext.invocationId; String functionCallId = toolContext.functionCallId().get(); // ToolContext에만 해당 System.out.println("로그: 호출= " + invId + " 에이전트= " + agentName); } -
초기 사용자 입력 접근: 현재 호출을 시작한 메시지를 다시 참조합니다.
# 의사 코드: 콜백에서 from google.adk.agents.callback_context import CallbackContext def check_initial_intent(callback_context: CallbackContext, **kwargs): initial_text = "N/A" if callback_context.user_content and callback_context.user_content.parts: initial_text = callback_context.user_content.parts[0].text or "텍스트 아닌 입력" print(f"이 호출은 사용자 입력으로 시작되었습니다: '{initial_text}'") # 의사 코드: 에이전트의 _run_async_impl에서 # async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]: # if ctx.user_content and ctx.user_content.parts: # initial_text = ctx.user_content.parts[0].text # print(f"초기 쿼리를 기억하는 에이전트 로직: {initial_text}") # ...import ( "google.golang.org/adk/agent" "google.golang.org/genai" ) // Pseudocode: In a Callback func logInitialUserInput(ctx agent.CallbackContext) (*genai.Content, error) { userContent := ctx.UserContent() if userContent != nil && len(userContent.Parts) > 0 { if text := userContent.Parts[0].Text; text != "" { fmt.Printf("User's initial input for this turn: '%s'\n", text) } } return nil, nil // No modification }// 의사 코드: 콜백에서 import com.google.adk.agents.CallbackContext; public void checkInitialIntent(CallbackContext callbackContext){ String initialText = "N/A"; if((!(callbackContext.userContent().isEmpty())) && (!(callbackContext.userContent().parts.isEmpty()))){ initialText = cbx.userContent().get().parts().get().get(0).text().get(); ... System.out.println("이 호출은 사용자 입력으로 시작되었습니다: " + initialText) } }
상태 관리¶
상태는 메모리와 데이터 흐름에 매우 중요합니다. CallbackContext 또는 ToolContext를 사용하여 상태를 수정하면 변경 사항이 프레임워크에 의해 자동으로 추적되고 유지됩니다.
-
작동 방식:
callback_context.state['my_key'] = my_value또는tool_context.state['my_key'] = my_value에 값을 쓰면, 이 변경 사항이 현재 단계의 이벤트와 연관된EventActions.state_delta에 추가됩니다. 그런 다음SessionService는 이벤트를 영구 저장할 때 이러한 델타를 적용합니다. -
도구 간 데이터 전달
# 의사 코드: 도구 1 - 사용자 ID 가져오기 from google.adk.tools import ToolContext import uuid def get_user_profile(tool_context: ToolContext) -> dict: user_id = str(uuid.uuid4()) # ID 가져오기 시뮬레이션 # 다음 도구를 위해 ID를 상태에 저장 tool_context.state["temp:current_user_id"] = user_id return {"profile_status": "ID 생성됨"} # 의사 코드: 도구 2 - 상태에서 사용자 ID 사용 def get_user_orders(tool_context: ToolContext) -> dict: user_id = tool_context.state.get("temp:current_user_id") if not user_id: return {"error": "상태에서 사용자 ID를 찾을 수 없음"} print(f"사용자 ID에 대한 주문 가져오기: {user_id}") # ... user_id를 사용하여 주문을 가져오는 로직 ... return {"orders": ["order123", "order456"]}import "google.golang.org/adk/tool" // Pseudocode: Tool 1 - Fetches user ID type GetUserProfileArgs struct { } func getUserProfile(tc tool.Context, input GetUserProfileArgs) (string, error) { // A random user ID for demonstration purposes userID := "random_user_456" // Save the ID to state for the next tool if err := tc.State().Set("temp:current_user_id", userID); err != nil { return "", fmt.Errorf("failed to set user ID in state: %w", err) } return "ID generated", nil } // Pseudocode: Tool 2 - Uses user ID from state type GetUserOrdersArgs struct { } type getUserOrdersResult struct { Orders []string `json:"orders"` } func getUserOrders(tc tool.Context, input GetUserOrdersArgs) (*getUserOrdersResult, error) { userID, err := tc.State().Get("temp:current_user_id") if err != nil { return &getUserOrdersResult{}, fmt.Errorf("user ID not found in state") } fmt.Printf("Fetching orders for user ID: %v\n", userID) // ... logic to fetch orders using user_id ... return &getUserOrdersResult{Orders: []string{"order123", "order456"}}, nil }// 의사 코드: 도구 1 - 사용자 ID 가져오기 import com.google.adk.tools.ToolContext; import java.util.UUID; public Map<String, String> getUserProfile(ToolContext toolContext){ String userId = UUID.randomUUID().toString(); // 다음 도구를 위해 ID를 상태에 저장 tool_context.state().put("temp:current_user_id", user_id); return Map.of("profile_status", "ID 생성됨"); } // 의사 코드: 도구 2 - 상태에서 사용자 ID 사용 public Map<String, String> getUserOrders(ToolContext toolContext){ String userId = toolContext.state().get("temp:current_user_id"); if(userId.isEmpty()){ return Map.of("error", "상태에서 사용자 ID를 찾을 수 없음"); } System.out.println("사용자 ID에 대한 주문 가져오기: " + userId); // ... user_id를 사용하여 주문을 가져오는 로직 ... return Map.of("orders", "order123"); } -
사용자 선호도 업데이트:
# 의사 코드: 도구나 콜백이 선호도를 식별 from google.adk.tools import ToolContext # 또는 CallbackContext def set_user_preference(tool_context: ToolContext, preference: str, value: str) -> dict: # 영구 SessionService를 사용하는 경우 사용자 수준 상태에 'user:' 접두사 사용 state_key = f"user:{preference}" tool_context.state[state_key] = value print(f"사용자 선호도 '{preference}'을(를) '{value}'(으)로 설정") return {"status": "선호도 업데이트됨"}import "google.golang.org/adk/tool" // Pseudocode: Tool or Callback identifies a preference type setUserPreferenceArgs struct { Preference string `json:"preference" jsonschema:"The name of the preference to set."` Value string `json:"value" jsonschema:"The value to set for the preference."` } type setUserPreferenceResult struct { Status string `json:"status"` } func setUserPreference(tc tool.Context, args setUserPreferenceArgs) (setUserPreferenceResult, error) { // Use 'user:' prefix for user-level state (if using a persistent SessionService) stateKey := fmt.Sprintf("user:%s", args.Preference) if err := tc.State().Set(stateKey, args.Value); err != nil { return setUserPreferenceResult{}, fmt.Errorf("failed to set preference in state: %w", err) } fmt.Printf("Set user preference '%s' to '%s'\n", args.Preference, args.Value) return setUserPreferenceResult{Status: "Preference updated"}, nil }// 의사 코드: 도구나 콜백이 선호도를 식별 import com.google.adk.tools.ToolContext; // 또는 CallbackContext public Map<String, String> setUserPreference(ToolContext toolContext, String preference, String value){ // 영구 SessionService를 사용하는 경우 사용자 수준 상태에 'user:' 접두사 사용 String stateKey = "user:" + preference; toolContext.state().put(stateKey, value); System.out.println("사용자 선호도 '" + preference + "'을(를) '" + value + "'(으)로 설정"); return Map.of("status", "선호도 업데이트됨"); } -
상태 접두사: 기본 상태는 세션에만 해당되지만,
app:및user:와 같은 접두사는 영구SessionService구현(DatabaseSessionService또는VertexAiSessionService등)과 함께 사용하여 더 넓은 범위(앱 전체 또는 세션 간 사용자 전체)를 나타낼 수 있습니다.temp:는 현재 호출 내에서만 관련된 데이터를 나타낼 수 있습니다.
아티팩트 작업¶
세션과 관련된 파일이나 큰 데이터 덩어리를 처리하려면 아티팩트를 사용하세요. 일반적인 사용 사례: 업로드된 문서 처리.
-
문서 요약기 예제 흐름:
-
참조 수집 (예: 설정 도구나 콜백에서): 문서의 전체 내용이 아닌 경로나 URI를 아티팩트로 저장합니다.
# 의사 코드: 콜백이나 초기 도구에서 from google.adk.agents.callback_context import CallbackContext # 또는 ToolContext from google.genai import types def save_document_reference(context: CallbackContext, file_path: str) -> None: # file_path가 "gs://my-bucket/docs/report.pdf" 또는 "/local/path/to/report.pdf"와 같다고 가정 try: # 경로/URI 텍스트를 포함하는 Part 생성 artifact_part = types.Part(text=file_path) version = context.save_artifact("document_to_summarize.txt", artifact_part) print(f"문서 참조 '{file_path}'를 아티팩트 버전 {version}(으)로 저장함") # 다른 도구에서 필요하면 파일 이름을 상태에 저장 context.state["temp:doc_artifact_name"] = "document_to_summarize.txt" except ValueError as e: print(f"아티팩트 저장 오류: {e}") # 예: 아티팩트 서비스가 구성되지 않음 except Exception as e: print(f"아티팩트 참조 저장 중 예기치 않은 오류 발생: {e}") # 사용 예: # save_document_reference(callback_context, "gs://my-bucket/docs/report.pdf")import ( "google.golang.org/adk/tool" "google.golang.org/genai" ) // Adapt the saveDocumentReference callback into a tool for this example. type saveDocRefArgs struct { FilePath string `json:"file_path" jsonschema:"The path to the file to save."` } type saveDocRefResult struct { Status string `json:"status"` } func saveDocRef(tc tool.Context, args saveDocRefArgs) (saveDocRefResult, error) { artifactPart := genai.NewPartFromText(args.FilePath) _, err := tc.Artifacts().Save(tc, "document_to_summarize.txt", artifactPart) if err != nil { return saveDocRefResult{}, err } fmt.Printf("Saved document reference '%s' as artifact\n", args.FilePath) if err := tc.State().Set("temp:doc_artifact_name", "document_to_summarize.txt"); err != nil { return saveDocRefResult{}, fmt.Errorf("failed to set artifact name in state") } return saveDocRefResult{"Reference saved"}, nil }// 의사 코드: 콜백이나 초기 도구에서 import com.google.adk.agents.CallbackContext; import com.google.genai.types.Content; import com.google.genai.types.Part; pubic void saveDocumentReference(CallbackContext context, String filePath){ // file_path가 "gs://my-bucket/docs/report.pdf" 또는 "/local/path/to/report.pdf"와 같다고 가정 try{ // 경로/URI 텍스트를 포함하는 Part 생성 Part artifactPart = types.Part(filePath) Optional<Integer> version = context.saveArtifact("document_to_summarize.txt", artifactPart) System.out.println("문서 참조 " + filePath + "를 아티팩트 버전 " + version + "(으)로 저장함"); // 다른 도구에서 필요하면 파일 이름을 상태에 저장 context.state().put("temp:doc_artifact_name", "document_to_summarize.txt"); } catch(Exception e){ System.out.println("아티팩트 참조 저장 중 예기치 않은 오류 발생: " + e); } } // 사용 예: // saveDocumentReference(context, "gs://my-bucket/docs/report.pdf") -
요약기 도구: 아티팩트를 로드하여 경로/URI를 얻고, 적절한 라이브러리를 사용하여 실제 문서 내용을 읽고, 요약한 후 결과를 반환합니다.
# 의사 코드: 요약기 도구 함수에서 from google.adk.tools import ToolContext from google.genai import types # google.cloud.storage나 내장 open 같은 라이브러리가 사용 가능하다고 가정 # 'summarize_text' 함수가 존재한다고 가정 # from my_summarizer_lib import summarize_text def summarize_document_tool(tool_context: ToolContext) -> dict: artifact_name = tool_context.state.get("temp:doc_artifact_name") if not artifact_name: return {"error": "상태에서 문서 아티팩트 이름을 찾을 수 없습니다."} try: # 1. 경로/URI를 포함하는 아티팩트 파트를 로드합니다 artifact_part = tool_context.load_artifact(artifact_name) if not artifact_part or not artifact_part.text: return {"error": f"아티팩트를 로드할 수 없거나 아티팩트에 텍스트 경로가 없습니다: {artifact_name}"} file_path = artifact_part.text print(f"로드된 문서 참조: {file_path}") # 2. 실제 문서 내용을 읽습니다 (ADK 컨텍스트 외부) document_content = "" if file_path.startswith("gs://"): # 예: GCS 클라이언트 라이브러리를 사용하여 다운로드/읽기 # from google.cloud import storage # client = storage.Client() # blob = storage.Blob.from_string(file_path, client=client) # document_content = blob.download_as_text() # 또는 형식에 따라 바이트 pass # 실제 GCS 읽기 로직으로 대체 elif file_path.startswith("/"): # 예: 로컬 파일 시스템 사용 with open(file_path, 'r', encoding='utf-8') as f: document_content = f.read() else: return {"error": f"지원되지 않는 파일 경로 스킴: {file_path}"} # 3. 내용을 요약합니다 if not document_content: return {"error": "문서 내용을 읽지 못했습니다."} # summary = summarize_text(document_content) # 요약 로직 호출 summary = f"{file_path}의 내용 요약" # 플레이스홀더 return {"summary": summary} except ValueError as e: return {"error": f"아티팩트 서비스 오류: {e}"} except FileNotFoundError: return {"error": f"로컬 파일을 찾을 수 없음: {file_path}"} # except Exception as e: # GCS 등에 대한 특정 예외 처리 # return {"error": f"문서 읽기 오류 {file_path}: {e}"}import "google.golang.org/adk/tool" // Pseudocode: In the Summarizer tool function type summarizeDocumentArgs struct{} type summarizeDocumentResult struct { Summary string `json:"summary"` } func summarizeDocumentTool(tc tool.Context, input summarizeDocumentArgs) (summarizeDocumentResult, error) { artifactName, err := tc.State().Get("temp:doc_artifact_name") if err != nil { return summarizeDocumentResult{}, fmt.Errorf("No document artifact name found in state") } // 1. Load the artifact part containing the path/URI artifactPart, err := tc.Artifacts().Load(tc, artifactName.(string)) if err != nil { return summarizeDocumentResult{}, err } if artifactPart.Part.Text == "" { return summarizeDocumentResult{}, fmt.Errorf("Could not load artifact or artifact has no text path.") } filePath := artifactPart.Part.Text fmt.Printf("Loaded document reference: %s\n", filePath) // 2. Read the actual document content (outside ADK context) // In a real implementation, you would use a GCS client or local file reader. documentContent := "This is the fake content of the document at " + filePath _ = documentContent // Avoid unused variable error. // 3. Summarize the content summary := "Summary of content from " + filePath // Placeholder return summarizeDocumentResult{Summary: summary}, nil }// 의사 코드: 요약기 도구 함수에서 import com.google.adk.tools.ToolContext; import com.google.genai.types.Content; import com.google.genai.types.Part; public Map<String, String> summarizeDocumentTool(ToolContext toolContext){ String artifactName = toolContext.state().get("temp:doc_artifact_name"); if(artifactName.isEmpty()){ return Map.of("error", "상태에서 문서 아티팩트 이름을 찾을 수 없습니다."); } try{ // 1. 경로/URI를 포함하는 아티팩트 파트를 로드합니다 Maybe<Part> artifactPart = toolContext.loadArtifact(artifactName); if((artifactPart == null) || (artifactPart.text().isEmpty())){ return Map.of("error", "아티팩트를 로드할 수 없거나 아티팩트에 텍스트 경로가 없습니다: " + artifactName); } filePath = artifactPart.text(); System.out.println("로드된 문서 참조: " + filePath); // 2. 실제 문서 내용을 읽습니다 (ADK 컨텍스트 외부) String documentContent = ""; if(filePath.startsWith("gs://")){ // 예: GCS 클라이언트 라이브러리를 사용하여 documentContent로 다운로드/읽기 pass; // 실제 GCS 읽기 로직으로 대체 } else if(){ // 예: 로컬 파일 시스템을 사용하여 documentContent로 다운로드/읽기 } else{ return Map.of("error", "지원되지 않는 파일 경로 스킴: " + filePath); } // 3. 내용을 요약합니다 if(documentContent.isEmpty()){ return Map.of("error", "문서 내용을 읽지 못했습니다."); } // summary = summarizeText(documentContent) // 요약 로직 호출 summary = filePath + "의 내용 요약"; // 플레이스홀더 return Map.of("summary", summary); } catch(IllegalArgumentException e){ return Map.of("error", "아티팩트 서비스 오류 " + filePath + e); } catch(FileNotFoundException e){ return Map.of("error", "로컬 파일을 찾을 수 없음 " + filePath + e); } catch(Exception e){ return Map.of("error", "문서 읽기 오류 " + filePath + e); } }
-
-
아티팩트 목록 조회: 사용 가능한 파일이 무엇인지 확인합니다.
# 의사 코드: 도구 함수에서 from google.adk.tools import ToolContext def check_available_docs(tool_context: ToolContext) -> dict: try: artifact_keys = tool_context.list_artifacts() print(f"사용 가능한 아티팩트: {artifact_keys}") return {"available_docs": artifact_keys} except ValueError as e: return {"error": f"아티팩트 서비스 오류: {e}"}import "google.golang.org/adk/tool" // Pseudocode: In a tool function type checkAvailableDocsArgs struct{} type checkAvailableDocsResult struct { AvailableDocs []string `json:"available_docs"` } func checkAvailableDocs(tc tool.Context, args checkAvailableDocsArgs) (checkAvailableDocsResult, error) { artifactKeys, err := tc.Artifacts().List(tc) if err != nil { return checkAvailableDocsResult{}, err } fmt.Printf("Available artifacts: %v\n", artifactKeys) return checkAvailableDocsResult{AvailableDocs: artifactKeys.FileNames}, nil }// 의사 코드: 도구 함수에서 import com.google.adk.tools.ToolContext; public Map<String, String> checkAvailableDocs(ToolContext toolContext){ try{ Single<List<String>> artifactKeys = toolContext.listArtifacts(); System.out.println("사용 가능한 아티팩트: " + artifactKeys.tostring()); return Map.of("availableDocs", "artifactKeys"); } catch(IllegalArgumentException e){ return Map.of("error", "아티팩트 서비스 오류: " + e); } }
도구 인증 처리¶
도구에 필요한 API 키나 다른 자격 증명을 안전하게 관리합니다.
# 의사 코드: 인증이 필요한 도구
from google.adk.tools import ToolContext
from google.adk.auth import AuthConfig # 적절한 AuthConfig가 정의되었다고 가정
# 필요한 인증 설정 정의 (예: OAuth, API 키)
MY_API_AUTH_CONFIG = AuthConfig(...)
AUTH_STATE_KEY = "user:my_api_credential" # 검색된 자격 증명을 저장할 키
def call_secure_api(tool_context: ToolContext, request_data: str) -> dict:
# 1. 자격 증명이 이미 상태에 있는지 확인
credential = tool_context.state.get(AUTH_STATE_KEY)
if not credential:
# 2. 없다면 요청
print("자격 증명을 찾을 수 없어 요청합니다...")
try:
tool_context.request_credential(MY_API_AUTH_CONFIG)
# 프레임워크가 이벤트 생성을 처리합니다. 도구 실행은 이 턴에서 여기서 멈춥니다.
return {"status": "인증이 필요합니다. 자격 증명을 제공해 주세요."}
except ValueError as e:
return {"error": f"인증 오류: {e}"} # 예: function_call_id 누락
except Exception as e:
return {"error": f"자격 증명 요청 실패: {e}"}
# 3. 자격 증명이 있다면 (요청 후 이전 턴에서 왔을 수 있음)
# 또는 외부에서 인증 흐름이 완료된 후의 후속 호출이라면
try:
# 선택적으로 필요시 재검증/재검색하거나 직접 사용
# 외부 흐름이 방금 완료되었다면 자격 증명을 검색할 수 있음
auth_credential_obj = tool_context.get_auth_response(MY_API_AUTH_CONFIG)
api_key = auth_credential_obj.api_key # 또는 access_token 등
# 세션 내 미래의 호출을 위해 상태에 다시 저장
tool_context.state[AUTH_STATE_KEY] = auth_credential_obj.model_dump() # 검색된 자격 증명 유지
print(f"검색된 자격 증명을 사용하여 데이터로 API 호출: {request_data}")
# ... api_key를 사용하여 실제 API 호출 ...
api_result = f"{request_data}에 대한 API 결과"
return {"result": api_result}
except Exception as e:
# 자격 증명 사용/검색 오류 처리
print(f"자격 증명 사용 오류: {e}")
# 자격 증명이 유효하지 않으면 상태 키를 지울 수도 있음
# tool_context.state[AUTH_STATE_KEY] = None
return {"error": "자격 증명 사용 실패"}
request_credential은 도구를 일시 중지시키고 인증 필요성을 알립니다. 사용자/시스템이 자격 증명을 제공하면, 후속 호출에서 get_auth_response(또는 상태를 다시 확인)를 통해 도구가 계속 진행할 수 있습니다. tool_context.function_call_id는 프레임워크에 의해 요청과 응답을 연결하는 데 암묵적으로 사용됩니다.
메모리 활용¶
과거 또는 외부 소스에서 관련 정보에 접근합니다.
# 의사 코드: 메모리 검색을 사용하는 도구
from google.adk.tools import ToolContext
def find_related_info(tool_context: ToolContext, topic: str) -> dict:
try:
search_results = tool_context.search_memory(f"{topic}에 대한 정보")
if search_results.results:
print(f"'{topic}'에 대해 {len(search_results.results)}개의 메모리 결과 발견")
# search_results.results 처리 (SearchMemoryResponseEntry임)
top_result_text = search_results.results[0].text
return {"memory_snippet": top_result_text}
else:
return {"message": "관련된 메모리를 찾을 수 없음."}
except ValueError as e:
return {"error": f"메모리 서비스 오류: {e}"} # 예: 서비스가 구성되지 않음
except Exception as e:
return {"error": f"메모리 검색 중 예기치 않은 오류 발생: {e}"}
고급: 직접적인 InvocationContext 사용¶
대부분의 상호작용은 CallbackContext나 ToolContext를 통해 이루어지지만, 때로는 에이전트의 핵심 로직(_run_async_impl/_run_live_impl)이 직접 접근해야 할 때가 있습니다.
# 의사 코드: 에이전트의 _run_async_impl 내부
from google.adk.agents import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event
from typing import AsyncGenerator
class MyControllingAgent(BaseAgent):
async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:
# 예: 특정 서비스가 사용 가능한지 확인
if not ctx.memory_service:
print("이 호출에 메모리 서비스를 사용할 수 없습니다.")
# 잠재적으로 에이전트 행동 변경
# 예: 특정 조건에 따른 조기 종료
if ctx.session.state.get("critical_error_flag"):
print("치명적인 오류 감지, 호출 종료.")
ctx.end_invocation = True # 프레임워크에 처리 중단 신호
yield Event(author=self.name, invocation_id=ctx.invocation_id, content="치명적인 오류로 인해 중지합니다.")
return # 이 에이전트의 실행 중지
# ... 일반적인 에이전트 처리 ...
yield # ... 이벤트 ...
ctx.end_invocation = True를 설정하는 것은 에이전트나 그 콜백/도구 내에서(각각의 컨텍스트 객체를 통해 기본 InvocationContext의 플래그에 접근하여 수정 가능) 전체 요청-응답 주기를 정상적으로 중지시키는 방법입니다.
핵심 요약 및 모범 사례¶
- 올바른 컨텍스트 사용: 항상 제공되는 가장 구체적인 컨텍스트 객체를 사용하세요(
ToolContext는 도구/도구-콜백에서,CallbackContext는 에이전트/모델-콜백에서,ReadonlyContext는 해당되는 곳에서). 전체InvocationContext(ctx)는_run_async_impl/_run_live_impl에서 꼭 필요할 때만 직접 사용하세요. - 데이터 흐름을 위한 상태:
context.state는 호출 내에서 데이터를 공유하고, 선호도를 기억하며, 대화 메모리를 관리하는 주요 방법입니다. 영구 저장소를 사용할 때는 접두사(app:,user:,temp:)를 신중하게 사용하세요. - 파일을 위한 아티팩트: 파일 참조(경로나 URI 등)나 큰 데이터 덩어리를 관리할 때는
context.save_artifact와context.load_artifact를 사용하세요. 참조를 저장하고, 필요할 때 내용을 로드하세요. - 추적되는 변경 사항: 컨텍스트 메서드를 통해 상태나 아티팩트를 수정하면 현재 단계의
EventActions에 자동으로 연결되고SessionService에 의해 처리됩니다. - 단순하게 시작: 먼저
state와 기본적인 아티팩트 사용에 집중하세요. 필요가 더 복잡해지면 인증, 메모리, 그리고 고급InvocationContext필드(라이브 스트리밍용 필드 등)를 탐색하세요.
이러한 컨텍스트 객체를 이해하고 효과적으로 사용함으로써 ADK로 더 정교하고, 상태를 유지하며, 유능한 에이전트를 구축할 수 있습니다.