콘텐츠로 이동

LLM 에이전트

ADK에서 지원Python v0.1.0Go v0.1.0Java v0.1.0

LlmAgent(종종 Agent로 축약)는 ADK의 핵심 구성 요소로, 애플리케이션의 "사고" 부분을 담당합니다. LlmAgent는 대규모 언어 모델(LLM)의 강력한 성능을 활용하여 추론, 자연어 이해, 의사 결정, 응답 생성, 도구와 상호 작용을 수행합니다.

미리 정의된 실행 경로를 따르는 결정적인(deterministic) 워크플로 에이전트와 달리, LlmAgent의 행동은 비결정적(non-deterministic)입니다. LLM을 사용하여 명령과 컨텍스트를 해석하고, 동적으로 진행 방법을 결정하거나, 어떤 도구를 사용할지(사용한다면), 또는 다른 에이전트에게 제어를 넘길지 결정합니다.

효과적인 LlmAgent를 구축하려면 에이전트의 정체성을 정의하고, 명령을 통해 행동을 명확하게 안내하며, 필요한 도구와 기능을 갖추는 것이 포함됩니다.

에이전트의 정체성과 목적 정의하기

먼저, 에이전트가 무엇이며 어떤 목적을 갖는지 설정해야 합니다.

  • name (필수): 모든 에이전트는 고유한 문자열 식별자가 필요합니다. 이 name은 내부 작업, 특히 에이전트들이 서로를 참조하거나 작업을 위임해야 하는 다중 에이전트 시스템에서 매우 중요합니다. 에이전트의 기능을 반영하는 설명적인 이름(예: customer_support_router, billing_inquiry_agent)을 선택하세요. user와 같은 예약된 이름은 피해야 합니다.

  • description (선택 사항, 다중 에이전트에 권장): 에이전트의 기능에 대한 간결한 요약을 제공합니다. 이 설명은 주로 다른 LLM 에이전트가 이 에이전트에게 작업을 라우팅해야 할지 결정하는 데 사용됩니다. 동료 에이전트와 구별될 수 있도록 구체적으로 작성하세요(예: "결제 에이전트"가 아닌 "현재 청구서에 대한 문의 처리").

  • model (필수): 이 에이전트의 추론을 구동할 기본 LLM을 지정합니다. 이는 "gemini-2.0-flash"와 같은 문자열 식별자입니다. 모델 선택은 에이전트의 능력, 비용, 성능에 영향을 미칩니다. 사용 가능한 옵션과 고려 사항은 모델 페이지를 참조하세요.

# 예시: 기본 정체성 정의
capital_agent = LlmAgent(
    model="gemini-2.0-flash",
    name="capital_agent",
    description="주어진 국가의 수도에 대한 사용자 질문에 답변합니다."
    # instruction과 tools는 다음에 추가됩니다
)
// Example: Defining the basic identity
agent, err := llmagent.New(llmagent.Config{
    Name:        "capital_agent",
    Model:       model,
    Description: "Answers user questions about the capital city of a given country.",
    // instruction and tools will be added next
})
// 예시: 기본 정체성 정의
LlmAgent capitalAgent =
    LlmAgent.builder()
        .model("gemini-2.0-flash")
        .name("capital_agent")
        .description("주어진 국가의 수도에 대한 사용자 질문에 답변합니다.")
        // instruction과 tools는 다음에 추가됩니다
        .build();

에이전트 안내하기: 명령 (instruction)

instruction 매개변수는 LlmAgent의 행동을 형성하는 데 있어 아마도 가장 중요한 요소일 것입니다. 이는 에이전트에게 다음을 알려주는 문자열(또는 문자열을 반환하는 함수)입니다:

  • 핵심 과업 또는 목표.
  • 성격 또는 페르소나 (예: "당신은 도움이 되는 어시스턴트입니다," "당신은 재치 있는 해적입니다").
  • 행동에 대한 제약 조건 (예: "X에 대한 질문에만 답변하세요," "Y는 절대 공개하지 마세요").
  • tools를 어떻게 그리고 언제 사용해야 하는지. 각 도구의 목적과 호출되어야 하는 상황을 설명하여 도구 자체의 설명을 보충해야 합니다.
  • 원하는 출력 형식 (예: "JSON으로 응답하세요," "글머리 기호 목록으로 제공하세요").

효과적인 명령을 위한 팁:

  • 명확하고 구체적으로 작성: 모호함을 피하세요. 원하는 행동과 결과를 명확하게 기술하세요.
  • 마크다운 사용: 복잡한 명령의 가독성을 높이기 위해 제목, 목록 등을 사용하세요.
  • 예시 제공 (Few-Shot): 복잡한 작업이나 특정 출력 형식의 경우, 명령에 직접 예시를 포함하세요.
  • 도구 사용 안내: 도구를 나열만 하지 말고, 에이전트가 언제 그리고 사용해야 하는지 설명하세요.

상태(State):

  • 명령은 문자열 템플릿이며, {var} 구문을 사용하여 동적 값을 명령에 삽입할 수 있습니다.
  • {var}var라는 이름의 상태 변수 값을 삽입하는 데 사용됩니다.
  • {artifact.var}var라는 이름의 아티팩트(artifact) 텍스트 내용을 삽입하는 데 사용됩니다.
  • 상태 변수나 아티팩트가 존재하지 않으면 에이전트는 오류를 발생시킵니다. 오류를 무시하려면 변수 이름 뒤에 ?를 붙여 {var?}와 같이 사용할 수 있습니다.
# 예시: 명령 추가
capital_agent = LlmAgent(
    model="gemini-2.0-flash",
    name="capital_agent",
    description="주어진 국가의 수도에 대한 사용자 질문에 답변합니다.",
    instruction="""당신은 국가의 수도를 알려주는 에이전트입니다.
사용자가 국가의 수도를 물으면:
1. 사용자 쿼리에서 국가 이름을 식별합니다.
2. `get_capital_city` 도구를 사용하여 수도를 찾습니다.
3. 사용자에게 수도를 명확하게 언급하며 응답합니다.
예시 쿼리: "{country}의 수도는 어디인가요?"
예시 응답: "프랑스의 수도는 파리입니다."
""",
    # tools는 다음에 추가됩니다
)
    // Example: Adding instructions
    agent, err := llmagent.New(llmagent.Config{
        Name:        "capital_agent",
        Model:       model,
        Description: "Answers user questions about the capital city of a given country.",
        Instruction: `You are an agent that provides the capital city of a country.
When a user asks for the capital of a country:
1. Identify the country name from the user's query.
2. Use the 'get_capital_city' tool to find the capital.
3. Respond clearly to the user, stating the capital city.
Example Query: "What's the capital of {country}?"
Example Response: "The capital of France is Paris."`,
        // tools will be added next
    })
// 예시: 명령 추가
LlmAgent capitalAgent =
    LlmAgent.builder()
        .model("gemini-2.0-flash")
        .name("capital_agent")
        .description("주어진 국가의 수도에 대한 사용자 질문에 답변합니다.")
        .instruction(
            """
            당신은 국가의 수도를 알려주는 에이전트입니다.
            사용자가 국가의 수도를 물으면:
            1. 사용자 쿼리에서 국가 이름을 식별합니다.
            2. `get_capital_city` 도구를 사용하여 수도를 찾습니다.
            3. 사용자에게 수도를 명확하게 언급하며 응답합니다.
            예시 쿼리: "{country}의 수도는 어디인가요?"
            예시 응답: "프랑스의 수도는 파리입니다."
            """)
        // tools는 다음에 추가됩니다
        .build();

(참고: 시스템의 모든 에이전트에 적용되는 명령의 경우, 루트 에이전트의 global_instruction 사용을 고려하세요. 자세한 내용은 다중 에이전트 섹션에서 설명합니다.)

에이전트 갖추기: 도구 (tools)

도구는 LlmAgent에게 LLM의 내장된 지식이나 추론 능력을 넘어서는 기능을 제공합니다. 도구를 통해 에이전트는 외부 세계와 상호 작용하고, 계산을 수행하며, 실시간 데이터를 가져오거나 특정 작업을 실행할 수 있습니다.

  • tools (선택 사항): 에이전트가 사용할 수 있는 도구 목록을 제공합니다. 목록의 각 항목은 다음 중 하나일 수 있습니다:
    • 네이티브 함수 또는 메서드 (FunctionTool로 래핑됨). Python ADK는 네이티브 함수를 자동으로 FunctionTool로 래핑하지만, Java에서는 FunctionTool.create(...)를 사용하여 명시적으로 메서드를 래핑해야 합니다.
    • BaseTool을 상속하는 클래스의 인스턴스.
    • 다른 에이전트의 인스턴스 (AgentTool, 에이전트 간 위임 가능 - 다중 에이전트 참조).

LLM은 함수/도구 이름, 설명(docstring이나 description 필드에서 가져옴), 그리고 매개변수 스키마를 사용하여 대화와 명령에 따라 어떤 도구를 호출할지 결정합니다.

# 도구 함수 정의
def get_capital_city(country: str) -> str:
  """주어진 국가의 수도를 검색합니다."""
  # 실제 로직으로 대체 (예: API 호출, 데이터베이스 조회)
  capitals = {"france": "Paris", "japan": "Tokyo", "canada": "Ottawa"}
  return capitals.get(country.lower(), f"죄송합니다. {country}의 수도는 알지 못합니다.")

# 에이전트에 도구 추가
capital_agent = LlmAgent(
    model="gemini-2.0-flash",
    name="capital_agent",
    description="주어진 국가의 수도에 대한 사용자 질문에 답변합니다.",
    instruction="""당신은 국가의 수도를 알려주는 에이전트입니다... (이전 명령 텍스트)""",
    tools=[get_capital_city] # 함수를 직접 제공
)
// Define a tool function
type getCapitalCityArgs struct {
    Country string `json:"country" jsonschema:"The country to get the capital of."`
}
getCapitalCity := func(ctx tool.Context, args getCapitalCityArgs) (map[string]any, error) {
    // Replace with actual logic (e.g., API call, database lookup)
    capitals := map[string]string{"france": "Paris", "japan": "Tokyo", "canada": "Ottawa"}
    capital, ok := capitals[strings.ToLower(args.Country)]
    if !ok {
        return nil, fmt.Errorf("Sorry, I don't know the capital of %s.", args.Country)
    }
    return map[string]any{"result": capital}, nil
}

// Add the tool to the agent
capitalTool, err := functiontool.New(
    functiontool.Config{
        Name:        "get_capital_city",
        Description: "Retrieves the capital city for a given country.",
    },
    getCapitalCity,
)
if err != nil {
    log.Fatal(err)
}
agent, err := llmagent.New(llmagent.Config{
    Name:        "capital_agent",
    Model:       model,
    Description: "Answers user questions about the capital city of a given country.",
    Instruction: "You are an agent that provides the capital city of a country... (previous instruction text)",
    Tools:       []tool.Tool{capitalTool},
})
// 도구 함수 정의
// 주어진 국가의 수도를 검색합니다.
public static Map<String, Object> getCapitalCity(
        @Schema(name = "country", description = "수도를 찾을 국가")
        String country) {
  // 실제 로직으로 대체 (예: API 호출, 데이터베이스 조회)
  Map<String, String> countryCapitals = new HashMap<>();
  countryCapitals.put("canada", "Ottawa");
  countryCapitals.put("france", "Paris");
  countryCapitals.put("japan", "Tokyo");

  String result =
          countryCapitals.getOrDefault(
                  country.toLowerCase(), "죄송합니다, " + country + "의 수도를 찾을 수 없습니다.");
  return Map.of("result", result); // 도구는 Map을 반환해야 합니다
}

// 에이전트에 도구 추가
FunctionTool capitalTool = FunctionTool.create(experiment.getClass(), "getCapitalCity");
LlmAgent capitalAgent =
    LlmAgent.builder()
        .model("gemini-2.0-flash")
        .name("capital_agent")
        .description("주어진 국가의 수도에 대한 사용자 질문에 답변합니다.")
        .instruction("당신은 국가의 수도를 알려주는 에이전트입니다... (이전 명령 텍스트)")
        .tools(capitalTool) // FunctionTool로 래핑된 함수 제공
        .build();

도구에 대한 자세한 내용은 도구 섹션에서 알아보세요.

고급 설정 및 제어

핵심 매개변수 외에도 LlmAgent는 더 세밀한 제어를 위한 여러 옵션을 제공합니다.

LLM 생성 구성 (generate_content_config)

generate_content_config를 사용하여 기본 LLM이 응답을 생성하는 방식을 조정할 수 있습니다.

  • generate_content_config (선택 사항): google.genai.types.GenerateContentConfig의 인스턴스를 전달하여 temperature(무작위성), max_output_tokens(응답 길이), top_p, top_k 및 안전 설정과 같은 매개변수를 제어합니다.
from google.genai import types

agent = LlmAgent(
    # ... 다른 매개변수들
    generate_content_config=types.GenerateContentConfig(
        temperature=0.2, # 더 결정적인 출력
        max_output_tokens=250,
        safety_settings=[
            types.SafetySetting(
                category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
                threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
            )
        ]
    )
)
import "google.golang.org/genai"

temperature := float32(0.2)
agent, err := llmagent.New(llmagent.Config{
    Name:  "gen_config_agent",
    Model: model,
    GenerateContentConfig: &genai.GenerateContentConfig{
        Temperature:     &temperature,
        MaxOutputTokens: 250,
    },
})
import com.google.genai.types.GenerateContentConfig;

LlmAgent agent =
    LlmAgent.builder()
        // ... 다른 매개변수들
        .generateContentConfig(GenerateContentConfig.builder()
            .temperature(0.2F) // 더 결정적인 출력
            .maxOutputTokens(250)
            .build())
        .build();

데이터 구조화 (input_schema, output_schema, output_key)

LLM Agent와 구조화된 데이터 교환이 필요한 시나리오를 위해 ADK는 스키마 정의를 사용하여 예상 입력 및 원하는 출력 형식을 정의하는 메커니즘을 제공합니다.

  • input_schema (선택 사항): 예상 입력 구조를 나타내는 스키마를 정의합니다. 설정된 경우, 이 에이전트에 전달되는 사용자 메시지 내용은 이 스키마를 준수하는 JSON 문자열이어야 합니다. 명령은 사용자나 이전 에이전트를 그에 맞게 안내해야 합니다.

  • output_schema (선택 사항): 원하는 출력 구조를 나타내는 스키마를 정의합니다. 설정된 경우, 에이전트의 최종 응답은 이 스키마를 준수하는 JSON 문자열이어야 합니다.

  • output_key (선택 사항): 문자열 키를 제공합니다. 설정된 경우, 에이전트의 최종 응답 텍스트 내용은 이 키 아래 세션의 상태 사전에 자동으로 저장됩니다. 이는 에이전트 간 또는 워크플로 단계 간에 결과를 전달하는 데 유용합니다.

    • Python에서는 session.state[output_key] = agent_response_text와 같이 보일 수 있습니다.
    • Java에서는 session.state().put(outputKey, agentResponseText)와 같습니다.
    • Golang에서는 콜백 핸들러 내에서 ctx.State().Set(output_key, agentResponseText)와 같습니다.

입력 및 출력 스키마는 일반적으로 Pydantic BaseModel입니다.

from pydantic import BaseModel, Field

class CapitalOutput(BaseModel):
    capital: str = Field(description="국가의 수도.")

structured_capital_agent = LlmAgent(
    # ... name, model, description
    instruction="""당신은 수도 정보 에이전트입니다. 국가가 주어지면 수도를 포함하는 JSON 객체로만 응답하세요. 형식: {"capital": "capital_name"}""",
    output_schema=CapitalOutput, # JSON 출력 강제
    output_key="found_capital"  # 결과를 state['found_capital']에 저장
    # 여기서는 tools=[get_capital_city]를 효과적으로 사용할 수 없음
)

입력 및 출력 스키마는 google.genai.types.Schema 객체입니다.

capitalOutput := &genai.Schema{
    Type:        genai.TypeObject,
    Description: "Schema for capital city information.",
    Properties: map[string]*genai.Schema{
        "capital": {
            Type:        genai.TypeString,
            Description: "The capital city of the country.",
        },
    },
}

agent, err := llmagent.New(llmagent.Config{
    Name:         "structured_capital_agent",
    Model:        model,
    Description:  "Provides capital information in a structured format.",
    Instruction:  `You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {"capital": "capital_name"}`,
    OutputSchema: capitalOutput,
    OutputKey:    "found_capital",
    // Cannot use the capitalTool tool effectively here
})

입력 및 출력 스키마는 google.genai.types.Schema 객체입니다.

private static final Schema CAPITAL_OUTPUT =
    Schema.builder()
        .type("OBJECT")
        .description("수도 정보 스키마.")
        .properties(
            Map.of(
                "capital",
                Schema.builder()
                    .type("STRING")
                    .description("국가의 수도.")
                    .build()))
        .build();

LlmAgent structuredCapitalAgent =
    LlmAgent.builder()
        // ... name, model, description
        .instruction(
                "당신은 수도 정보 에이전트입니다. 국가가 주어지면 수도를 포함하는 JSON 객체로만 응답하세요. 형식: {\"capital\": \"capital_name\"}")
        .outputSchema(capitalOutput) // JSON 출력 강제
        .outputKey("found_capital") // 결과를 state.get("found_capital")에 저장
        // 여기서는 tools(getCapitalCity)를 효과적으로 사용할 수 없음
        .build();

컨텍스트 관리 (include_contents)

에이전트가 이전 대화 기록을 수신할지 여부를 제어합니다.

  • include_contents (선택 사항, 기본값: 'default'): contents(기록)가 LLM으로 전송될지 여부를 결정합니다.
    • 'default': 에이전트는 관련 대화 기록을 수신합니다.
    • 'none': 에이전트는 이전 contents를 받지 않습니다. 현재 명령과 현재 턴에 제공된 입력에만 기반하여 작동합니다 (상태 비저장 작업이나 특정 컨텍스트 강제에 유용).
stateless_agent = LlmAgent(
    # ... 다른 매개변수들
    include_contents='none'
)
import "google.golang.org/adk/agent/llmagent"

agent, err := llmagent.New(llmagent.Config{
    Name:            "stateless_agent",
    Model:           model,
    IncludeContents: llmagent.IncludeContentsNone,
})
import com.google.adk.agents.LlmAgent.IncludeContents;

LlmAgent statelessAgent =
    LlmAgent.builder()
        // ... 다른 매개변수들
        .includeContents(IncludeContents.NONE)
        .build();

플래너(Planner)

ADK에서 지원Python v0.1.0

planner (선택 사항): BasePlanner 인스턴스를 할당하여 실행 전에 다단계 추론 및 계획을 활성화합니다. 두 가지 주요 플래너가 있습니다:

  • BuiltInPlanner: 모델의 내장 계획 기능(예: Gemini의 사고 기능)을 활용합니다. 자세한 내용과 예시는 Gemini Thinking을 참조하세요.

    여기서 thinking_budget 매개변수는 응답 생성 시 사용할 사고 토큰 수를 모델에 안내합니다. include_thoughts 매개변수는 모델이 원시적인 생각과 내부 추론 과정을 응답에 포함할지 여부를 제어합니다.

    from google.adk import Agent
    from google.adk.planners import BuiltInPlanner
    from google.genai import types
    
    my_agent = Agent(
        model="gemini-2.5-flash",
        planner=BuiltInPlanner(
            thinking_config=types.ThinkingConfig(
                include_thoughts=True,
                thinking_budget=1024,
            )
        ),
        # ... 여기에 도구 추가
    )
    
  • PlanReActPlanner: 이 플래너는 모델이 출력에서 특정 구조를 따르도록 지시합니다: 먼저 계획을 세우고, 그런 다음 행동(도구 호출 등)을 실행하고, 단계에 대한 추론을 제공합니다. 특히 내장된 "사고" 기능이 없는 모델에 유용합니다.

    from google.adk import Agent
    from google.adk.planners import PlanReActPlanner
    
    my_agent = Agent(
        model="gemini-2.0-flash",
        planner=PlanReActPlanner(),
        # ... 여기에 도구 추가
    )
    

    에이전트의 응답은 구조화된 형식을 따릅니다:

    [user]: ai news
    [google_search_agent]: /*PLANNING*/
    1. 인공 지능 관련 최신 업데이트 및 헤드라인을 얻기 위해 "최신 AI 뉴스"로 구글 검색을 수행합니다.
    2. 검색 결과의 정보를 종합하여 최근 AI 뉴스 요약을 제공합니다.
    
    /*ACTION*/
    /*REASONING*/
    검색 결과는 회사 개발, 연구 돌파구 및 응용 프로그램과 같은 다양한 측면을 다루는 최근 AI 뉴스에 대한 포괄적인 개요를 제공합니다. 사용자의 요청에 답변하기에 충분한 정보가 있습니다.
    
    /*FINAL_ANSWER*/
    최근 AI 뉴스 요약입니다:
    ....
    

코드 실행

ADK에서 지원Python v0.1.0
  • code_executor (선택 사항): BaseCodeExecutor 인스턴스를 제공하여 에이전트가 LLM의 응답에서 발견된 코드 블록을 실행할 수 있도록 합니다. (도구/내장 도구 참조).

내장 플래너 사용 예시:

# (주석 생략, 원문 코드와 동일)
...

종합 예시

코드

다음은 완전한 기본 capital_agent입니다:

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# --- Full example code demonstrating LlmAgent with Tools vs. Output Schema ---
import json # Needed for pretty printing dicts
import asyncio 

from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from pydantic import BaseModel, Field

# --- 1. Define Constants ---
APP_NAME = "agent_comparison_app"
USER_ID = "test_user_456"
SESSION_ID_TOOL_AGENT = "session_tool_agent_xyz"
SESSION_ID_SCHEMA_AGENT = "session_schema_agent_xyz"
MODEL_NAME = "gemini-2.0-flash"

# --- 2. Define Schemas ---

# Input schema used by both agents
class CountryInput(BaseModel):
    country: str = Field(description="The country to get information about.")

# Output schema ONLY for the second agent
class CapitalInfoOutput(BaseModel):
    capital: str = Field(description="The capital city of the country.")
    # Note: Population is illustrative; the LLM will infer or estimate this
    # as it cannot use tools when output_schema is set.
    population_estimate: str = Field(description="An estimated population of the capital city.")

# --- 3. Define the Tool (Only for the first agent) ---
def get_capital_city(country: str) -> str:
    """Retrieves the capital city of a given country."""
    print(f"\n-- Tool Call: get_capital_city(country='{country}') --")
    country_capitals = {
        "united states": "Washington, D.C.",
        "canada": "Ottawa",
        "france": "Paris",
        "japan": "Tokyo",
    }
    result = country_capitals.get(country.lower(), f"Sorry, I couldn't find the capital for {country}.")
    print(f"-- Tool Result: '{result}' --")
    return result

# --- 4. Configure Agents ---

# Agent 1: Uses a tool and output_key
capital_agent_with_tool = LlmAgent(
    model=MODEL_NAME,
    name="capital_agent_tool",
    description="Retrieves the capital city using a specific tool.",
    instruction="""You are a helpful agent that provides the capital city of a country using a tool.
The user will provide the country name in a JSON format like {"country": "country_name"}.
1. Extract the country name.
2. Use the `get_capital_city` tool to find the capital.
3. Respond clearly to the user, stating the capital city found by the tool.
""",
    tools=[get_capital_city],
    input_schema=CountryInput,
    output_key="capital_tool_result", # Store final text response
)

# Agent 2: Uses output_schema (NO tools possible)
structured_info_agent_schema = LlmAgent(
    model=MODEL_NAME,
    name="structured_info_agent_schema",
    description="Provides capital and estimated population in a specific JSON format.",
    instruction=f"""You are an agent that provides country information.
The user will provide the country name in a JSON format like {{"country": "country_name"}}.
Respond ONLY with a JSON object matching this exact schema:
{json.dumps(CapitalInfoOutput.model_json_schema(), indent=2)}
Use your knowledge to determine the capital and estimate the population. Do not use any tools.
""",
    # *** NO tools parameter here - using output_schema prevents tool use ***
    input_schema=CountryInput,
    output_schema=CapitalInfoOutput, # Enforce JSON output structure
    output_key="structured_info_result", # Store final JSON response
)

# --- 5. Set up Session Management and Runners ---
session_service = InMemorySessionService()

# Create a runner for EACH agent
capital_runner = Runner(
    agent=capital_agent_with_tool,
    app_name=APP_NAME,
    session_service=session_service
)
structured_runner = Runner(
    agent=structured_info_agent_schema,
    app_name=APP_NAME,
    session_service=session_service
)

# --- 6. Define Agent Interaction Logic ---
async def call_agent_and_print(
    runner_instance: Runner,
    agent_instance: LlmAgent,
    session_id: str,
    query_json: str
):
    """Sends a query to the specified agent/runner and prints results."""
    print(f"\n>>> Calling Agent: '{agent_instance.name}' | Query: {query_json}")

    user_content = types.Content(role='user', parts=[types.Part(text=query_json)])

    final_response_content = "No final response received."
    async for event in runner_instance.run_async(user_id=USER_ID, session_id=session_id, new_message=user_content):
        # print(f"Event: {event.type}, Author: {event.author}") # Uncomment for detailed logging
        if event.is_final_response() and event.content and event.content.parts:
            # For output_schema, the content is the JSON string itself
            final_response_content = event.content.parts[0].text

    print(f"<<< Agent '{agent_instance.name}' Response: {final_response_content}")

    current_session = await session_service.get_session(app_name=APP_NAME,
                                                  user_id=USER_ID,
                                                  session_id=session_id)
    stored_output = current_session.state.get(agent_instance.output_key)

    # Pretty print if the stored output looks like JSON (likely from output_schema)
    print(f"--- Session State ['{agent_instance.output_key}']: ", end="")
    try:
        # Attempt to parse and pretty print if it's JSON
        parsed_output = json.loads(stored_output)
        print(json.dumps(parsed_output, indent=2))
    except (json.JSONDecodeError, TypeError):
         # Otherwise, print as string
        print(stored_output)
    print("-" * 30)


# --- 7. Run Interactions ---
async def main():
    # Create separate sessions for clarity, though not strictly necessary if context is managed
    print("--- Creating Sessions ---")
    await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID_TOOL_AGENT)
    await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID_SCHEMA_AGENT)

    print("--- Testing Agent with Tool ---")
    await call_agent_and_print(capital_runner, capital_agent_with_tool, SESSION_ID_TOOL_AGENT, '{"country": "France"}')
    await call_agent_and_print(capital_runner, capital_agent_with_tool, SESSION_ID_TOOL_AGENT, '{"country": "Canada"}')

    print("\n\n--- Testing Agent with Output Schema (No Tool Use) ---")
    await call_agent_and_print(structured_runner, structured_info_agent_schema, SESSION_ID_SCHEMA_AGENT, '{"country": "France"}')
    await call_agent_and_print(structured_runner, structured_info_agent_schema, SESSION_ID_SCHEMA_AGENT, '{"country": "Japan"}')

# --- Run the Agent ---
# Note: In Colab, you can directly use 'await' at the top level.
# If running this code as a standalone Python script, you'll need to use asyncio.run() or manage the event loop.
if __name__ == "__main__":
    asyncio.run(main())    
package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "strings"

    "google.golang.org/adk/agent"
    "google.golang.org/adk/agent/llmagent"
    "google.golang.org/adk/model/gemini"
    "google.golang.org/adk/runner"
    "google.golang.org/adk/session"
    "google.golang.org/adk/tool"
    "google.golang.org/adk/tool/functiontool"

    "google.golang.org/genai"
)

// --- Main Runnable Example ---

const (
    modelName = "gemini-2.0-flash"
    appName   = "agent_comparison_app"
    userID    = "test_user_456"
)

type getCapitalCityArgs struct {
    Country string `json:"country" jsonschema:"The country to get the capital of."`
}

// getCapitalCity retrieves the capital city of a given country.
func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) (map[string]any, error) {
    fmt.Printf("\n-- Tool Call: getCapitalCity(country='%s') --\n", args.Country)
    capitals := map[string]string{
        "united states": "Washington, D.C.",
        "canada":        "Ottawa",
        "france":        "Paris",
        "japan":         "Tokyo",
    }
    capital, ok := capitals[strings.ToLower(args.Country)]
    if !ok {
        result := fmt.Sprintf("Sorry, I couldn't find the capital for %s.", args.Country)
        fmt.Printf("-- Tool Result: '%s' --\n", result)
        return nil, errors.New(result)
    }
    fmt.Printf("-- Tool Result: '%s' --\n", capital)
    return map[string]any{"result": capital}, nil
}

// callAgent is a helper function to execute an agent with a given prompt and handle its output.
func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) {
    fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", a.Name(), prompt)
    // Create an in-memory session service to manage agent state.
    sessionService := session.InMemoryService()

    // Create a new session for the agent interaction.
    sessionCreateResponse, err := sessionService.Create(ctx, &session.CreateRequest{
        AppName: appName,
        UserID:  userID,
    })
    if err != nil {
        log.Fatalf("Failed to create the session service: %v", err)
    }

    session := sessionCreateResponse.Session

    // Configure the runner with the application name, agent, and session service.
    config := runner.Config{
        AppName:        appName,
        Agent:          a,
        SessionService: sessionService,
    }

    // Create a new runner instance.
    r, err := runner.New(config)
    if err != nil {
        log.Fatalf("Failed to create the runner: %v", err)
    }

    // Prepare the user's message to send to the agent.
    sessionID := session.ID()
    userMsg := &genai.Content{
        Parts: []*genai.Part{
            genai.NewPartFromText(prompt),
        },
        Role: string(genai.RoleUser),
    }

    // Run the agent and process the streaming events.
    for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{
        StreamingMode: agent.StreamingModeSSE,
    }) {
        if err != nil {
            fmt.Printf("\nAGENT_ERROR: %v\n", err)
        } else if event.Partial {
            // Print partial responses as they are received.
            for _, p := range event.Content.Parts {
                fmt.Print(p.Text)
            }
        }
    }

    // After the run, check if there's an expected output key in the session state.
    if outputKey != "" {
        storedOutput, error := session.State().Get(outputKey)
        if error == nil {
            // Pretty-print the stored output if it's a JSON string.
            fmt.Printf("\n--- Session State ['%s']: ", outputKey)
            storedString, isString := storedOutput.(string)
            if isString {
                var prettyJSON map[string]interface{}
                if err := json.Unmarshal([]byte(storedString), &prettyJSON); err == nil {
                    indentedJSON, err := json.MarshalIndent(prettyJSON, "", "  ")
                    if err == nil {
                        fmt.Println(string(indentedJSON))
                    } else {
                        fmt.Println(storedString)
                    }
                } else {
                    fmt.Println(storedString)
                }
            } else {
                fmt.Println(storedOutput)
            }
            fmt.Println(strings.Repeat("-", 30))
        }
    }
}

func main() {
    ctx := context.Background()

    model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{})
    if err != nil {
        log.Fatalf("Failed to create model: %v", err)
    }

    capitalTool, err := functiontool.New(
        functiontool.Config{
            Name:        "get_capital_city",
            Description: "Retrieves the capital city for a given country.",
        },
        getCapitalCity,
    )
    if err != nil {
        log.Fatalf("Failed to create function tool: %v", err)
    }

    countryInputSchema := &genai.Schema{
        Type:        genai.TypeObject,
        Description: "Input for specifying a country.",
        Properties: map[string]*genai.Schema{
            "country": {
                Type:        genai.TypeString,
                Description: "The country to get information about.",
            },
        },
        Required: []string{"country"},
    }

    capitalAgentWithTool, err := llmagent.New(llmagent.Config{
        Name:        "capital_agent_tool",
        Model:       model,
        Description: "Retrieves the capital city using a specific tool.",
        Instruction: `You are a helpful agent that provides the capital city of a country using a tool.
The user will provide the country name in a JSON format like {"country": "country_name"}.
1. Extract the country name.
2. Use the 'get_capital_city' tool to find the capital.
3. Respond clearly to the user, stating the capital city found by the tool.`,
        Tools:       []tool.Tool{capitalTool},
        InputSchema: countryInputSchema,
        OutputKey:   "capital_tool_result",
    })
    if err != nil {
        log.Fatalf("Failed to create capital agent with tool: %v", err)
    }

    capitalInfoOutputSchema := &genai.Schema{
        Type:        genai.TypeObject,
        Description: "Schema for capital city information.",
        Properties: map[string]*genai.Schema{
            "capital": {
                Type:        genai.TypeString,
                Description: "The capital city of the country.",
            },
            "population_estimate": {
                Type:        genai.TypeString,
                Description: "An estimated population of the capital city.",
            },
        },
        Required: []string{"capital", "population_estimate"},
    }
    schemaJSON, _ := json.Marshal(capitalInfoOutputSchema)
    structuredInfoAgentSchema, err := llmagent.New(llmagent.Config{
        Name:        "structured_info_agent_schema",
        Model:       model,
        Description: "Provides capital and estimated population in a specific JSON format.",
        Instruction: fmt.Sprintf(`You are an agent that provides country information.
The user will provide the country name in a JSON format like {"country": "country_name"}.
Respond ONLY with a JSON object matching this exact schema:
%s
Use your knowledge to determine the capital and estimate the population. Do not use any tools.`, string(schemaJSON)),
        InputSchema:  countryInputSchema,
        OutputSchema: capitalInfoOutputSchema,
        OutputKey:    "structured_info_result",
    })
    if err != nil {
        log.Fatalf("Failed to create structured info agent: %v", err)
    }

    fmt.Println("--- Testing Agent with Tool ---")
    callAgent(ctx, capitalAgentWithTool, "capital_tool_result", `{"country": "France"}`)
    callAgent(ctx, capitalAgentWithTool, "capital_tool_result", `{"country": "Canada"}`)

    fmt.Println("\n\n--- Testing Agent with Output Schema (No Tool Use) ---")
    callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "France"}`)
    callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "Japan"}`)
}
// --- Full example code demonstrating LlmAgent with Tools vs. Output Schema ---

import com.google.adk.agents.LlmAgent;
import com.google.adk.events.Event;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.adk.tools.Annotations;
import com.google.adk.tools.FunctionTool;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import com.google.genai.types.Schema;
import io.reactivex.rxjava3.core.Flowable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class LlmAgentExample {

  // --- 1. Define Constants ---
  private static final String MODEL_NAME = "gemini-2.0-flash";
  private static final String APP_NAME = "capital_agent_tool";
  private static final String USER_ID = "test_user_456";
  private static final String SESSION_ID_TOOL_AGENT = "session_tool_agent_xyz";
  private static final String SESSION_ID_SCHEMA_AGENT = "session_schema_agent_xyz";

  // --- 2. Define Schemas ---

  // Input schema used by both agents
  private static final Schema COUNTRY_INPUT_SCHEMA =
      Schema.builder()
          .type("OBJECT")
          .description("Input for specifying a country.")
          .properties(
              Map.of(
                  "country",
                  Schema.builder()
                      .type("STRING")
                      .description("The country to get information about.")
                      .build()))
          .required(List.of("country"))
          .build();

  // Output schema ONLY for the second agent
  private static final Schema CAPITAL_INFO_OUTPUT_SCHEMA =
      Schema.builder()
          .type("OBJECT")
          .description("Schema for capital city information.")
          .properties(
              Map.of(
                  "capital",
                  Schema.builder()
                      .type("STRING")
                      .description("The capital city of the country.")
                      .build(),
                  "population_estimate",
                  Schema.builder()
                      .type("STRING")
                      .description("An estimated population of the capital city.")
                      .build()))
          .required(List.of("capital", "population_estimate"))
          .build();

  // --- 3. Define the Tool (Only for the first agent) ---
  // Retrieves the capital city of a given country.
  public static Map<String, Object> getCapitalCity(
      @Annotations.Schema(name = "country", description = "The country to get capital for")
      String country) {
    System.out.printf("%n-- Tool Call: getCapitalCity(country='%s') --%n", country);
    Map<String, String> countryCapitals = new HashMap<>();
    countryCapitals.put("united states", "Washington, D.C.");
    countryCapitals.put("canada", "Ottawa");
    countryCapitals.put("france", "Paris");
    countryCapitals.put("japan", "Tokyo");

    String result =
        countryCapitals.getOrDefault(
            country.toLowerCase(), "Sorry, I couldn't find the capital for " + country + ".");
    System.out.printf("-- Tool Result: '%s' --%n", result);
    return Map.of("result", result); // Tools must return a Map
  }

  public static void main(String[] args){
    LlmAgentExample agentExample = new LlmAgentExample();
    FunctionTool capitalTool = FunctionTool.create(agentExample.getClass(), "getCapitalCity");

    // --- 4. Configure Agents ---

    // Agent 1: Uses a tool and output_key
    LlmAgent capitalAgentWithTool =
        LlmAgent.builder()
            .model(MODEL_NAME)
            .name("capital_agent_tool")
            .description("Retrieves the capital city using a specific tool.")
            .instruction(
              """
              You are a helpful agent that provides the capital city of a country using a tool.
              1. Extract the country name.
              2. Use the `get_capital_city` tool to find the capital.
              3. Respond clearly to the user, stating the capital city found by the tool.
              """)
            .tools(capitalTool)
            .inputSchema(COUNTRY_INPUT_SCHEMA)
            .outputKey("capital_tool_result") // Store final text response
            .build();

    // Agent 2: Uses an output schema
    LlmAgent structuredInfoAgentSchema =
        LlmAgent.builder()
            .model(MODEL_NAME)
            .name("structured_info_agent_schema")
            .description("Provides capital and estimated population in a specific JSON format.")
            .instruction(
                String.format("""
                You are an agent that provides country information.
                Respond ONLY with a JSON object matching this exact schema: %s
                Use your knowledge to determine the capital and estimate the population. Do not use any tools.
                """, CAPITAL_INFO_OUTPUT_SCHEMA.toJson()))
            // *** NO tools parameter here - using output_schema prevents tool use ***
            .inputSchema(COUNTRY_INPUT_SCHEMA)
            .outputSchema(CAPITAL_INFO_OUTPUT_SCHEMA) // Enforce JSON output structure
            .outputKey("structured_info_result") // Store final JSON response
            .build();

    // --- 5. Set up Session Management and Runners ---
    InMemorySessionService sessionService = new InMemorySessionService();

    sessionService.createSession(APP_NAME, USER_ID, null, SESSION_ID_TOOL_AGENT).blockingGet();
    sessionService.createSession(APP_NAME, USER_ID, null, SESSION_ID_SCHEMA_AGENT).blockingGet();

    Runner capitalRunner = new Runner(capitalAgentWithTool, APP_NAME, null, sessionService);
    Runner structuredRunner = new Runner(structuredInfoAgentSchema, APP_NAME, null, sessionService);

    // --- 6. Run Interactions ---
    System.out.println("--- Testing Agent with Tool ---");
    agentExample.callAgentAndPrint(
        capitalRunner, capitalAgentWithTool, SESSION_ID_TOOL_AGENT, "{\"country\": \"France\"}");
    agentExample.callAgentAndPrint(
        capitalRunner, capitalAgentWithTool, SESSION_ID_TOOL_AGENT, "{\"country\": \"Canada\"}");

    System.out.println("\n\n--- Testing Agent with Output Schema (No Tool Use) ---");
    agentExample.callAgentAndPrint(
        structuredRunner,
        structuredInfoAgentSchema,
        SESSION_ID_SCHEMA_AGENT,
        "{\"country\": \"France\"}");
    agentExample.callAgentAndPrint(
        structuredRunner,
        structuredInfoAgentSchema,
        SESSION_ID_SCHEMA_AGENT,
        "{\"country\": \"Japan\"}");
  }

  // --- 7. Define Agent Interaction Logic ---
  public void callAgentAndPrint(Runner runner, LlmAgent agent, String sessionId, String queryJson) {
    System.out.printf(
        "%n>>> Calling Agent: '%s' | Session: '%s' | Query: %s%n",
        agent.name(), sessionId, queryJson);

    Content userContent = Content.fromParts(Part.fromText(queryJson));
    final String[] finalResponseContent = {"No final response received."};
    Flowable<Event> eventStream = runner.runAsync(USER_ID, sessionId, userContent);

    // Stream event response
    eventStream.blockingForEach(event -> {
          if (event.finalResponse() && event.content().isPresent()) {
            event
                .content()
                .get()
                .parts()
                .flatMap(parts -> parts.isEmpty() ? Optional.empty() : Optional.of(parts.get(0)))
                .flatMap(Part::text)
                .ifPresent(text -> finalResponseContent[0] = text);
          }
        });

    System.out.printf("<<< Agent '%s' Response: %s%n", agent.name(), finalResponseContent[0]);

    // Retrieve the session again to get the updated state
    Session updatedSession =
        runner
            .sessionService()
            .getSession(APP_NAME, USER_ID, sessionId, Optional.empty())
            .blockingGet();

    if (updatedSession != null && agent.outputKey().isPresent()) {
      // Print to verify if the stored output looks like JSON (likely from output_schema)
      System.out.printf("--- Session State ['%s']: ", agent.outputKey().get());
      }
  }
}

(이 예시는 핵심 개념을 보여줍니다. 더 복잡한 에이전트는 스키마, 컨텍스트 제어, 계획 등을 통합할 수 있습니다.)

관련 개념 (추후 다룰 주제)

이 페이지에서는 LlmAgent의 핵심 구성을 다루지만, 몇 가지 관련 개념은 더 고급 제어를 제공하며 다른 곳에서 자세히 설명합니다:

  • 콜백(Callbacks): before_model_callback, after_model_callback 등을 사용하여 실행 지점(모델 호출 전/후, 도구 호출 전/후)을 가로채기. 콜백 참조.
  • 다중 에이전트 제어: 계획(planner), 에이전트 전환 제어(disallow_transfer_to_parent, disallow_transfer_to_peers), 시스템 전반의 명령(global_instruction)을 포함한 에이전트 상호 작용의 고급 전략. 다중 에이전트 참조.