Skip to main content

Google Agent Development Kit(ADK): 실습 튜토리얼

Google의 Agent Development Kit (ADK)는 다양한 모델과 도구를 지원하며, 손쉬운 통합과 확장성, 그리고 프로덕션 환경에서의 관측 가능성을 염두에 두고 설계된 현대적이고 모듈형의 Python 프레임워크입니다. 복잡한 AI 기반 에이전트와 워크플로를 구축하고 오케스트레이션하며 트레이싱할 수 있도록 지원합니다. 본 문서는 AI 번역된 글입니다. 오역이 의심되는 부분은 댓글로 알려 주세요.
Created on September 12|Last edited on September 12
Google의 Agent Development Kit (ADK) 현대적인 프레임워크로, 개발과 배포를 간소화하도록 설계되었습니다 AI 기반 에이전트ADK는 정교한 기능을 갖춘 시스템을 구축할 때 수반되는 고유한 과제들을 해결합니다. 자율적인 에이전트형 시스템 모듈성, 재사용성, 유지보수성 같은 전통적 소프트웨어 공학의 원칙을 에이전트형 AI 아키텍처의 새로운 요구와 결합함으로써.
Agent Development Kit은 모델에 구애받지 않고 배포 방식에 유연한 설계를 통해 다양한 AI 모델과 인프라에서 동작한다는 점이 특징입니다. 특히 최적화 대상은 Gemini 및 Google Cloud 에코시스템(예: Vertex AI), 다양한 서드파티 AI 프레임워크와도 연동할 수 있습니다. 아키텍처는 근본적으로 이벤트 주도형이며, 에이전트의 의사결정, 워크플로 오케스트레이션, 그리고 지속성 및 보안 같은 시스템 수준의 과제를 명확히 분리하도록 설계되었습니다.
오늘은 에이전트를 구축할 때의 모범 사례를 살펴보고, Google의 ADK로 몇 가지 에이전트를 만들어 본 뒤, 다음 도구로 평가해 보겠습니다. W&B Weave.


목차



에이전트란 무엇인가요?

먼저 용어를 정리하겠습니다. AI 에이전트는 계획을 수립하고, 외부 도구를 활용하며, 기억을 유지하고, 시간에 따라 적응함으로써 특정 목표를 달성하도록 설계된 지능형 시스템입니다. 고정된 사전에 정의된 규칙을 따르는 전통적인 자동화와 달리, AI 에이전트는 정보를 동적으로 처리하고 의사 결정을 내리며, 받은 피드백을 바탕으로 접근 방식을 지속적으로 개선합니다.
챗봇이 주로 대화를 수행하며 매 단계마다 사용자 입력을 필요로 하는 반면, AI 에이전트는 사용자 입력과 무관하게 독립적으로 동작합니다. 단순히 응답을 생성하는 데 그치지 않고, 행동을 수행하고 외부 시스템과 상호작용하며 다단계 에이전트형 워크플로를 관리하기 지속적인 감독 없이
AI 에이전트를 구성하는 핵심 요소는 다음과 같습니다:
  • 도구: 기능을 확장하기 위해 API, 데이터베이스, 소프트웨어에 연결하세요.
  • 메모리: 작업 전반에 걸쳐 정보를 저장하여 일관성과 회상을 향상합니다.
  • 연속 학습: 과거 성과를 바탕으로 전략을 조정하고 정교화하세요.
  • 오케스트레이션: 다단계 프로세스를 관리하고, 작업을 분해하며, 다른 에이전트와 조율합니다

ADK의 기초

ADK로 구축을 시작하려면 먼저 시스템의 주요 지능형 엔터티인 에이전트를 정의합니다. 핵심 에이전트 유���은 다음과 같습니다:
  • LLM 에이전트: 이는 활용합니다 추론을 위한 대형 언어 모델, 언어 이해 및 워크플로 내 동적 라우팅을 수행합니다. 유연한 의사 결정을 위한 “두뇌” 역할을 하며, 자연어 인터페이스와 복잡한 로직에 필수적입니다.
  • 순차 에이전트: 미리 정의된 단계별 워크플로를 실행합니다. 각 작업이 엄격한 순서로 앞선 작업을 따르는 선형 파이프라인에 유용합니다
  • 병렬 에이전트: 여러 하위 작업을 동시에 실행하도록 조율하여, 병렬 처리가 가능한 작업의 처리량을 높입니다.
  • 루프 에이전트: 반복적인 처리나 개선 과정을 자동화합니다. 예를 들어, 특정 조건이 충족될 때까지 반복적으로 출력을 생성할 수 있습니다(예: 이미지 다섯 장을 생성하거나 품질 기준을 만족할 때까지 반복).
  • 사용자 정의 에이전트: 기존 패턴에 맞지 않는 고유한 요구 사항을 위해, 개발자는 특화된 에이전트를 직접 설계해 만들 수 있습니다.
ADK의 대표적인 특징은 다음과 같은 지원입니다 멀티 에이전트 아키텍처에이전트를 계층적으로 구성하고 서로를 “도구”처럼 활용할 수 있게 하여, 확장 가능한 위임과 풍부한 문제 분해를 가능하게 합니다.

상태, 메모리, 세션, 도구

ADK의 컨텍스트 처리 방식은 핵심 강점 중 하나입니다:
  • 세션: 사용자와 에이전트(들) 사이에서 진행 중인 단일 상호작용(또는 대화)을 나타냅니다. 세션은 “이벤트”(사용자 입력, 에이전트 응답, 도구 호출)의 순서를 캡슐화하고, 해당 교환이 이어지는 동안 연속성을 제공합니다. SessionService 관리자는 세션을 저장하고 조회하며, 휘발성(메모리 내)과 영구성(데이터베이스, Vertex AI) 옵션을 모두 지원합니다.
  • 상태세션(또는 사용자)에 연결된 가변 스크래치패드입니다. 상태는 현재 상호작용에만 관련된 실시간 키-값 데이터를 저장합니다(예: 사용자 선택, 대화 플래그, 장바구니 내용). 상태 업데이트는 이벤트에 의해 트리거되며, 원자성과 일관성을 보장하도록 관리됩니다.
  • 메모리: 장기적이며 세션 간에 공유되는 지식 저장소입니다. 메모리는 단일 세션을 넘어서는 정보나 컨텍스트를 검색할 수 있게 하여, 이전 상호작용을 조회하거나 외부 지식 코퍼스를 수집·통합할 수 있게 합니다. 메모리는 도구를 통해 접근하며, 키워드 검색 또는 의미 기반 검색을 사용할 수 있습니다(예: Vertex AI RAG).
  • 도구: 작업을 수행하기 위해 에이전트가 호출할 수 있는 플러그형 액션 또는 인터페이스입니다. 범위는 다음과 같습니다:
    • 사전 구축된 도구(예: Search 또는 Code Exec)
    • 사용자 정의 함수(개발자가 정의한 워크플로 또는 API 호출)
    • 서드파티 통합(LangChain, CrewAI 등)
    • 다른 에이전트(AgentTools), 재귀적 위임 지원
    • 클라우드 또는 외부 서비스에 접근하기 위한 Google Cloud 또는 OpenAPI 도구
“도구”라는 추상화는 에이전트가 외부 세계(예: API, 데이터베이스, 다른 에이전트)와 상호작용하고, 추론하며, 행동할 수 있게 해 주되, 그 행동을 에이전트의 핵심 로직에 직접 하드코딩하지 않아도 되도록 합니다.

아티팩트와 콜백

  • 아티팩트 에이전트가 실행되는 동안 생성되는 비문자·바이너리 출력물(예: 이미지, 문서, 파일, 기타 미디어)을 가리킵니다. ADK는 아티팩트 관리 ArtifactService를 통해 에이전트가 이벤트·상태 데이터와 분리하여 대용량 객체를 저장하고 조회할 수 있으며, 이는 멀티모달 데이터나 산출물이 포함된 워크플로에 필수적입니다.
  • 콜백 사용자가 정의하여 에이전트 실행 라이프사이클에 연결하는 훅입니다. 예를 들어, 개발자는 다음을 지정할 수 있습니다 before_agent_callback 또는 after_model_callback 에이전트 동작의 핵심 지점에서 로깅을 커스터마이즈하고, 추가 검증을 적용하거나, 맞춤 로직을 주입하는 기능입니다. 콜백은 가시성과 확장성을 높이며, 기본 에이전트 코드베이스를 변경하지 않고도 특화된 제어를 가능하게 합니다.
전반적으로 Google의 Agent Development Kit은 통합적이고 고도로 모듈화된 프레임워크를 제공하여 차세대 에이전트형 AI 애플리케이션 구축에이전트 기반의 구조화된 워크플로, 세션·상태·메모리를 통한 견고한 컨텍스트 및 메모리 관리, 도구와 에이전트 조합을 통한 확장성, 아티팩트·영속성·콜백을 통한 프로덕션 준비성을 강조함으로써, ADK는 전통적 소프트웨어 엔지니어링과 최첨단 AI 사이의 간극을 메워 줍니다. 단순한 챗봇을 구축하든 복잡한 멀티 에이전트 비즈니스 프로세스를 오케스트레이션하든, ADK는 확장 가능하고 유지 관리가 용이한 에이전트 개발에 필요한 도구, 추상화, 모범 사례를 제공합니다.
Google ADK를 시작하려면 Python 3.8 이상만 준비하면 됩니다. 터미널이나 명령 프롬프트를 열고 다음 명령을 실행하세요:
pip install google-adk weave google-genai
이렇게 하면 기본 Agent Development Kit이 설치됩니다. (이 튜토리얼에서는 Vertex AI를 사용할 예정입니다.) Vertex AI 설정에 대한 자세한 내용은 다음을 참조하세요: 기사 최근에 작성했습니다.

Google ADK로 기본 작성 에이전트 만들기

이 섹션에서는 Google의 Agent Development Kit (ADK)를 사용해 간단하지만 강력한 멀티 에이전트 파이프라인을 만들어 보겠습니다. ADK를 사용하면 다양한 종류의 에이전트를 조합해 각각 특정 작업을 처리하고 결과를 다음 단계로 전달하는 지능형 AI 워크플로를 구성할 수 있습니다. 이런 모듈식 접근 방식은 로직을 깔끔하고 유지 관리하기 쉽게 만들 뿐만 아니라, 각 에이전트가 자신의 전문 영역에 특화될 수 있도록 해 줍니다.
이번 예제는 흔한 시나리오를 다룹니다. 웹에서 주제를 조사하고, 그 결과를 바탕으로 짧은 요약을 작성한 뒤, 다른 에이전트가 초안을 품질과 사실 정확성 측면에서 비평하고, 마지막으로 그 피드백에 따라 텍스트를 수정합니다. 이 과정을 하나의 대형 언어 모델 프롬프트로도 만들 수 있지만, 협업하는 에이전트들의 체인으로 구조화하면 각 단계가 더 투명해지고, 재사용 가능하며, 디버깅이나 확장이 쉬워집니다.
우리는 LLM 에이전트와 순차 오케스트레이션 같은 ADK의 내장 에이전트 타입을 활용해 범위를 분명히 하고, 연구를 위해 Google Search를 도구로 연결하겠습니다. ADK의 웹 CLI 덕분에 세션이나 웹 서버를 직접 관리할 필요가 없습니다. 에이전트만 정의하면 나머지는 시스템이 처리하며, 편리한 브라우저 인터페이스를 통해 상호작용할 수 있습니다.
이제 에이전트를 정의하고 코드를 통해 서로 체이닝해 보겠습니다.
import sys
import base64
import asyncio

# ====== AGENTS CODE (always imported/defined) ======
from google.adk.agents import LlmAgent, SequentialAgent
from google.adk.agents.callback_context import CallbackContext
from google.genai.types import Part, UserContent
from google.adk.tools import google_search
from google.adk.runners import InMemoryRunner

import os
os.environ["GOOGLE_CLOUD_PROJECT"] = "your_project"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"


research_agent = LlmAgent(
name="Researcher",
model="gemini-2.5-flash",
instruction="""
If 'greeted' is not set in state, greet the user with "👋 Hello! I'm your assistant. Hope all is well.".
Then, conduct thorough web research on the subject given in the input.
Gather key facts, recent information, and relevant details about the topic.
Summarize your research findings in a comprehensive but organized manner.
This research will be used by other agents to write content, so be thorough and factual.
""",
output_key="research",
tools=[google_search],
)

generator_agent = LlmAgent(
name="DraftWriter",
model="gemini-2.5-flash",
instruction="""
Using the research findings stored in state['research'], write a short, factual paragraph about the subject.
Base your writing on the research provided - don't make up facts.
Be concise and clear. Your output should be plain text only.
""",
output_key="draft_text",
)

reviewer_agent = LlmAgent(
name="Critic",
model="gemini-2.5-flash",
instruction="""
Analyze the paragraph stored in state['draft_text'].
Also reference the research in state['research'] to check for factual accuracy.
Always give a detailed critique, even if the text is factually correct, comment on style, clarity, possible improvements, or anything that could make it better. If there are factual problems, mention those. Output a short critique, not just 'valid'.
""",
output_key="critique",
)

revision_agent = LlmAgent(
name="Rewriter",
model="gemini-2.5-flash",
instruction="""
Your goal is to revise the paragraph in state['draft_text'] based on the feedback in state['critique'].
You can also reference the research in state['research'] to ensure accuracy and completeness.
Output only the improved paragraph, rewritten as needed.
""",
output_key="revised_text",
)

def greet_on_first_message(callback_context: CallbackContext):
if not callback_context.state.get("greeted"):
callback_context.state["greeted"] = True
return None

root_agent = SequentialAgent(
name="ResearchWriteCritiqueRewrite",
before_agent_callback=greet_on_first_message,
sub_agents=[
research_agent,
generator_agent,
reviewer_agent,
revision_agent
]
)

# =========== ONLY DO THIS FOR CLI MODE ===========
if 'web' not in sys.argv:
# --- OTEL Weave tracing ---
WANDB_BASE_URL = "https://trace.wandb.ai"
PROJECT_ID = "byyoung3/adk_demov2"
WANDB_API_KEY = "your_api_key"
if not WANDB_API_KEY or not PROJECT_ID:
raise ValueError("Set WANDB_API_KEY and PROJECT_ID as variables!")

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry import trace

OTEL_EXPORTER_OTLP_ENDPOINT = f"{WANDB_BASE_URL}/otel/v1/traces"
AUTH = base64.b64encode(f"api:{WANDB_API_KEY}".encode()).decode()
OTEL_EXPORTER_OTLP_HEADERS = {
"Authorization": f"Basic {AUTH}",
"project_id": PROJECT_ID,
}
exporter = OTLPSpanExporter(
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
headers=OTEL_EXPORTER_OTLP_HEADERS,
)
tracer_provider = trace_sdk.TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)

runner = InMemoryRunner(agent=root_agent)

async def create_session():
return await runner.session_service.create_session(
app_name=runner.app_name,
user_id="terminal_user"
)
session = asyncio.run(create_session())

print("==== Research, Write, Critique, Rewrite Terminal ====")
print("Type a subject or question to research (or 'quit' to exit):")
while True:
try:
user_input = input('\nEnter subject: ').strip()
except EOFError:
break
if user_input.lower() == 'quit':
print("Goodbye!")
break

content = UserContent(parts=[Part(text=user_input)])

try:
for event in runner.run(
user_id=session.user_id,
session_id=session.id,
new_message=content
):
if not event or not getattr(event, "content", None):
print("> [ERROR: No event or no content in event!]")
continue
for part in getattr(event.content, "parts", []) or []:
if getattr(part, "text", None):
print('> ', part.text)
except Exception as e:
print(f"ERROR: {e}")

print("Session ended.")

Gemini 2.5 Pro는 복잡하고 심층적인 추론과 미묘한 지시의 이해에 최적화되어 있어, 정교한 분석과 창의적 생성이 필요한 작업에 가장 적합한 선택입니다. 반면 Gemini 2.5 Flash는 놀라운 속도와 효율성에 맞춰 설계되어, 빈번하고 온디맨드로 요청되는 작업에 빠른 응답을 제공합니다. 이번 데모에서는 Google Cloud 무료 등급에서 더 관대한 rate limit을 제공한다는 이유로 Gemini 2.5 Flash를 선택했습니다. 덕분에 즉각적인 비용 부담 없이 더 폭넓게 실험하고 개발할 수 있어, 최신 Gemini 모델 패밀리의 강력한 기능을 탐색하기 위한 훌륭하고 접근 가능한 출발점이 됩니다.
💡
이 멀�� 에이전트 파이프라인이 실제로 내부에서 무엇을 하는지 좀 더 자세히 살펴보겠습니다. 사용자가 웹 인터페이스나 터미널을 통해 어떤 주제나 질문을 제출하든, 해당 요청은 다음에 의해 처리됩니다. root_agent엄격하게 정의된 순서로 워크플로를 오케스트레이션합니다. 최초 상호작용에서는 greet_on_first_message 콜백 덕분에 친근한 환영 메시지가 트리거됩니다. 이 콜백은 에이전트가 보내는 모든 메시지 전에 실행되지만, 실제로는 최초로 전송되는 메시지에서만 사용됩니다.
파이프라인의 흐름은 다음으로 시작됩니다 Researcher 에이전트. 구성 요소로서 LLM 기반 연구 보조 도구이 에이전트는 통합된 웹 도구를 활용해 제공된 주제에 대한 최신 정보를 수집하고, 이를 간결하고 체계적인 요약으로 정리해 공유 세션 상태에 저장합니다. 이어서, DraftWriter 에이전트는 이렇게 수집한 결과를 바탕으로 정확하고 사실에 근거한 단락을 작성하여, 꾸며낸 내용이 아닌 실제 리서치에 기반한 초안을 제공합니다.
품질과 정확성을 보장하기 위해, Critic 에이전트는 이후 이 초안을 스타일, 명확성, 그리고 리서치와의 정합성 측면에서 검토합니다. 파이프라인의 견고한 글쓰기 프로세스에 필수적인 건설적인 피드백을 생성합니다. 마지막으로, Rewriter 에이전트는 이 비평(원본 리서치와 함께)을 활용해 단락을 수정·개선하여, 사실 관계의 수정과 문체적 향상이 모두 반영된 최종 버전을 완성합니다.
주목할 만한 기술적 세부 사항은 …의 사용입니다 output_key 각 에이전트마다 매개변수를 지정합니다. 이렇게 하면 모든 에이전트가 공유 세션 상태에서 특정 키(예: research, draft_text, critique)에 자신의 출력을 기록하도록 보장됩니다. 이러한 구조는 후속 에이전트가 이전 단계의 결과를 손쉽게 가져오게 하고, 정보 흐름을 명시적으로 유지하며, 디버깅과 확장성을 단순화합니다.
무대 뒤에서는 파이프라인이 Weave OTEL(오픈텔레메트리) 통합 Google ADK와 함께. 이 설정은 엔드 투 엔드 가시성: 각 에이전트가 자신의 작업을 실행할 때, 도구 호출, LLM 호출, 중간 상태를 포함한 상세한 트레이스가 Weave를 통해 Weights & Biases에 자동으로 기록됩니다. 즉, 모든 세션의 모든 단계를 W&B 대시보드에서 시각화하고, 디버그하며, 분석할 수 있어 워크플로 전반에 걸친 에이전트의 의사결정과 상호작용을 추적하는 데 도움이 됩니다.
에이전트를 배포하고 실행하는 방법은 두 가지입니다:
  • 웹 인터페이스에이전트 디렉터리(에이전트/ 폴더 수준)로 이동한 다음, 다음을 입력합니다 adk web 브라우저 기반 채팅 환경을 실행하는 명령으로, 대화형 UI를 통해 최종 사용자도 쉽게 접근할 수 있습니다.
  • 터미널 인터페이스간단히 실행하세요: python agent.py 명령줄에서 실행하세요. 그러면 Weave/OTEL 로깅이 활성화되고, 친절한 프롬프트가 표시되며, 터미널에서 에이전트와 직접 대화할 수 있습니다. 모든 에이전트 로직과 트레이스는 자동으로 W&B 대시보드로 전송되어 탐색할 수 있습니다.
전문화된 에이전트들을 체이닝하고 Weave OTEL 트레이싱으로 모든 동작을 포착하면, 협업형 인간 워크플로를 반영하는 모듈식·투명·고디버그 가능 파이프라인을 구축할 수 있어, 지속적인 개발과 문제 해결이 훨씬 수월해집니다.
아래의 Weave 스크린샷은 우리의 멀티 에이전트 글쓰기 파이프라인이 Weave 내에서 어떻게 시각화되는지를 보여줍니다. 파이프라인은 연구원, 초안 작성자, 비평가, 재작성자라는 네 가지 역할로 구성됩니다. 왼쪽에는 호출 트리가 보이며, 각 agent_run은 하나의 역할을 나타내고, 각 call_llm은 해당 역할의 모델 상호작용을 보여줍니다.
오른쪽에는 Weave가 Researcher 에이전트의 전체 프롬프트와 응답을 표시합니다. 여기에는 도구 메타데이터, 사용자 지시사항, 그리고 Gemini 2.5 Flash가 생성한 최종 출력이 포함됩니다. 출력에는 요청된 주제에 대한 핵심 사실과 구조화된 콘텐츠가 담겨 있으며, 다른 에이전트가 이를 기반으로 확장할 수 있도록 모델이 사실 기반 요약을 어떻게 종합하는지 보여줍니다.


AI 보험 상담 에이전트 구축

보험 청구는 예기치 않은 사고 직후 고객에게 혼란스럽고 스트레스를 유발할 수 있는 과정입니다. 청구인은 다양한 서류를 수집하고, 상세한 양식을 작성하며, 사진이나 기타 증빙 자료를 업로드해야 할 뿐 아니라, 중요한 사항이 누락되지 않았는지도 확인해야 합니다. 그러나 어떤 세부 정보나 파일이 필요한지, 또는 보험 기준에 부합하는 방식으로 증거를 확보하는 방법을 고객이 확신하지 못하는 경우가 흔합니다. 그 결과 양식이 불완전하거나 서류가 누락되고, 사진이 필요한 맥락이나 선명도를 충분히 보여 주지 못하는 일도 발생합니다.
그 결과, 보험사는 추가 정보나 정정 사항, 더 높은 품질의 이미지를 요청하기 위해 종종 고객에게 다시 연락해야 합니다. 이러한 반복적인 왕복 소통은 청구 처리 지연을 초래할 뿐 아니라 모든 이해관계자의 불만을 키웁니다. 온라인 포털이 있어도 절차가 불투명하고 부담스럽게 느껴져 피할 수 있는 실수가 발생하고, 청구 승인까지의 대기 시간도 길어질 수 있습니다.
이러한 과제를 해소하고 절차를 더 쉽고 투명하게 만들기 위해, 보험 설계사에게 제출하기 전에 고객이 자신의 제출물을 사전 점검할 수 있도록 설계된 AI 기반 어시스턴트를 만들어 보겠습니다.이 어시스턴트를 통해 고객은 청구서 PDF와 사고 사진을 업로드할 수 있으며, 시스템이 제출물을 자동으로 검토해 누락된 정보를 표시하고, 필요한 보완 사항을 제안하며, 증빙 자료가 표준 요건을 충족하는지 확인합니다.
이 모든 피드백은 명확한 요약으로 통합되어, 최종 제출 전에 고객이 실수를 바로잡고 누락된 정보를 보완할 수 있도록 도와줍니다. 처음부터 완전하고 정확한 정보를 더 쉽게 제공할 수 있게 함으로써, 이 어시스턴트는 청구 처리 속도를 높이고 고객과 에이전트 모두의 불필요한 스트레스를 줄여 줍니다.
아래 코드는 문서 분석, 사진 검증, 유용한 보고서 생성에 특화된 AI 에이전트들을 순차적으로 결합해, 고객이 충분하고 탄탄한 보험 청구서를 준비할 수 있도록 함께 작동하는 어시스턴트를 구현하는 방법을 보여 줍니다.
import os
import shutil
import uuid
import asyncio
import PyPDF2
import base64
from flask import Flask, request, render_template_string, jsonify
import asyncio
import os
import base64

from google.adk.agents import Agent, LlmAgent, SequentialAgent
from google.adk.runners import InMemoryRunner
from google.genai import types
from litellm import completion
import base64
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry import trace

from google import genai
from google.genai import types
from PIL import Image
from io import BytesIO



import os
# switch to Gemini
os.environ["GOOGLE_CLOUD_PROJECT"] = "your_project"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"


def init_weave_otel_adk(
wandb_api_key: str,
wandb_project_id: str,
base_url: str = "https://trace.wandb.ai"
):
"""
Initialize OpenTelemetry tracing for Weave + Google ADK.

Args:
wandb_api_key (str): Your W&B API key.
wandb_project_id (str): Your W&B entity/project (e.g. "myteam/myproject").
base_url (str): The Weave trace API base URL (default is "https://trace.wandb.ai").
"""
otel_endpoint = f"{base_url}/otel/v1/traces"
auth = base64.b64encode(f"api:{wandb_api_key}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"project_id": wandb_project_id,
}
exporter = OTLPSpanExporter(
endpoint=otel_endpoint,
headers=headers,
)
tracer_provider = trace_sdk.TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)

PROJECT_ID = "byyoung3/adk_demo" # e.g. youruser/yourproject
WANDB_API_KEY = "your_api_key" # from https://wandb.ai/authorize

init_weave_otel_adk(wandb_api_key=WANDB_API_KEY, wandb_project_id=PROJECT_ID)
import weave; weave.init("adk_demov4")


# ==== Setup directories (cleared every run) ====
PDF_DIR = "pdfs"
IMG_DIR = "imgs"

for dir_path in [PDF_DIR, IMG_DIR]:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
os.makedirs(dir_path, exist_ok=True)



@weave.op
async def analyze_pdf_claim_tool(tool_context) -> dict:
print("RUNNING PDF", flush=True)
pdfs = [os.path.join(PDF_DIR, f) for f in os.listdir(PDF_DIR) if f.lower().endswith('.pdf')]
if not pdfs:
return {"status": "no_pdf", "message": "No PDF uploaded."}
latest = max(pdfs, key=os.path.getmtime)

try:
with open(latest, "rb") as f:
reader = PyPDF2.PdfReader(f)
all_text = "\n".join(page.extract_text() or '' for page in reader.pages)
print("all Text: " + all_text, flush=True)
if not all_text.strip():
return {"status": "error", "message": "No readable text in PDF."}
except Exception as e:
return {"status": "error", "message": f"Failed to read PDF: {e}"}

prompt = (
"You are an insurance expert. Review the following insurance claim document and list several questions "
"an insurance company should ask the claimant to clarify missing, ambiguous, incomplete, or suspicious information.\n\n"
"Claim Document:\n"
"-----------------------\n"
f"{all_text[:8000]}"
"\n-----------------------\n"
"Questions to ask the claimant:"
)

try:
client = genai.Client()
resp = await asyncio.to_thread(
client.models.generate_content,
model="gemini-2.5-flash",
contents=[prompt]
)
ans = getattr(resp, "text", None) or "No response."
print(ans, flush=True)
return {
"status": "success",
"claim_filename": os.path.basename(latest),
"questions": ans,
}
except Exception as e:
return {"status": "error", "message": f"Error from Gemini: {e}"}



@weave.op
async def analyze_one_image(pil_image, fname, criteria, client):
try:
buf = BytesIO()
pil_image.save(buf, format="PNG")
img_bytes = buf.getvalue()
except Exception as e:
return {
"filename": fname,
"error": f"Failed to process image: {e}",
}

parts = [
types.Part.from_bytes(data=img_bytes, mime_type="image/png"),
criteria
]
try:
resp = await asyncio.to_thread(
client.models.generate_content,
model="gemini-2.5-flash",
contents=parts
)
ans = getattr(resp, "text", None) or "No response."
return {
"filename": fname,
"analysis": ans,
}
except Exception as e:
return {
"filename": fname,
"error": f"Error analyzing image: {str(e)}",

}
@weave.op
async def analyze_imgs_tool(tool_context) -> dict:
current_call = weave.get_current_call()
call_id = getattr(current_call, 'id', None)

print("RUNNING IMG (parallel)", flush=True)
criteria = (
"As an insurance claims photo analyst, verify if this photo meets ALL of the following:\n"
"1. The image is sharp and clear (no blur, good lighting).\n"
"2. There are NO photo filters applied, and the image is not cropped to hide context.\n"
"3. The photo shows surroundings to prove location (for example, the full car, not just a dent).\n"
"For each point, state YES or NO and why.\n"
"Then, give a one-line verdict: Would this photo be acceptable for an insurance claim? (Yes/No and reason)."
)

# --- Collect all PIL images and associated filenames
files = sorted([
f for f in os.listdir(IMG_DIR)
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp'))
])
if not files:
return {"status": "no_images", "message": "No images available to analyze."}

pil_images = []
for fname in files:
fpath = os.path.join(IMG_DIR, fname)
try:
pil_img = Image.open(fpath).convert("RGB")
pil_images.append((fname, pil_img))
except Exception as e:
print(f"Failed to load image {fname}: {e}", flush=True)

client = genai.Client()

# --- Now analyze_one_image takes (PIL image, filename)


# Gather all results concurrently, passing (PIL image, filename)
results = await asyncio.gather(
*(analyze_one_image(pil_img, fname, criteria=criteria, client=client) for fname, pil_img in pil_images)
)

print(results, flush=True)
return {"status": "success", "analyses": results, 'call_id': call_id}

# EXAMPLE: Flask wrapper (for HTTP error codes) for PDF endpoint
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/analyze_pdf_claim', methods=['POST'])
async def analyze_pdf_claim():
result = await analyze_pdf_claim_tool({})
# HTTP STATUS CODES
if result["status"] == "success":
return jsonify(result), 200
elif result["status"] == "no_pdf":
return jsonify(result), 400
else:
return jsonify(result), 500

@app.route('/analyze_images', methods=['POST'])
async def analyze_images():
result = await analyze_imgs_tool({})
if result["status"] == "success":
return jsonify(result), 200
elif result["status"] == "no_images":
return jsonify(result), 400
else:
return jsonify(result), 500


# ==== Individual Agents ====
pdf_agent = Agent(
model="gemini-2.5-flash",
name="pdf_claim_analyzer",
instruction="use the analyze_pdf_claim_tool and generate follow-up questions for unclear or missing information. Just call the tool to start. It will handle the analysis",
description="Analyzes PDF insurance claim documents.",
tools=[analyze_pdf_claim_tool],
output_key="pdf_feedback"
)

photo_agent = Agent(
model="gemini-2.5-flash",
name="photo_claim_analyzer",
instruction="1. Read the photos using the analyze_imgs_tool. Next, Analyze insurance claim photos for quality and compliance with evidence standards.",
description="Analyzes insurance claim photos for acceptability.",
tools=[analyze_imgs_tool],
output_key="photo_feedback"
)

# --- Revision Agent ---
summarizer_agent = LlmAgent(
name="Rewriter",
model="gemini-2.5-flash",
instruction="""
Your goal is to combine the reports from state['photo_feedback'] and state['pdf_feedback'].
Create a comprehensive insurance claim analysis report that includes:
1. PDF document analysis and recommended questions
2. Photo quality assessment and acceptability
3. Overall claim assessment and recommendations

Output only the improved combined report, rewritten as needed.
""",
output_key="data_summary"
)

coordinator_agent = SequentialAgent(
name="insurance_claim_pipeline",
sub_agents=[pdf_agent, photo_agent, summarizer_agent]
)

# Create runner and session
runner = InMemoryRunner(agent=coordinator_agent)

# Create session synchronously at startup
async def create_session():
return await runner.session_service.create_session(
app_name=runner.app_name, user_id="insurance_user"
)

session = asyncio.run(create_session())

# ==== FLASK UI ====
app = Flask(__name__)

COMBINED_HTML = """
<!DOCTYPE html>
<html>
<head>
<title>Insurance Claim Analyzer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family:sans-serif; background:#f3f5f9;}
.main { max-width:650px; margin:40px auto; background:white; border-radius:8px; box-shadow:0 3px 12px #ddd; padding: 30px; }
.upload-section { margin-bottom: 30px; padding: 20px; background:#f8f9fa; border-radius: 6px; }
input[type=file] { margin-bottom:18px; width: 100%; }
.result { background:#f3f7fa;margin:20px 0;padding:18px; border-radius:7px; font-size:1.05em;}
.err { color:red;}
.imglist img { max-width:80px; margin:4px; border-radius:6px;}
.analysis-section { margin:15px 0; padding:15px; border-left: 3px solid #4a9fd6; background:#fafbfc; }
button { padding:10px 22px; margin:5px; border:none; background:#4a9fd6; color:white; border-radius:6px; cursor:pointer;}
button:disabled { background:#ccc; cursor:not-allowed; }
.file-info { font-size:0.9em; color:#666; margin:5px 0; }
</style>
</head>
<body>
<div class="main">
<h2>🔍 Insurance Claim Analyzer</h2>
<p>Upload PDF claim documents and/or photos. The AI will analyze both types of evidence and provide a comprehensive assessment.</p>
<div class="upload-section">
<h3>📄 PDF Claims</h3>
<form id="pdfForm" enctype="multipart/form-data" onsubmit="return uploadPdf();">
<input id="pdfFile" type="file" accept="application/pdf">
<button id="pdfBtn" type="submit">Upload PDF</button>
</form>
<div id="pdfStatus" class="file-info"></div>
</div>

<div class="upload-section">
<h3>📸 Claim Photos</h3>
<form id="photoForm" enctype="multipart/form-data" onsubmit="return uploadPhotos();">
<input id="photoFiles" type="file" accept="image/*" multiple>
<button id="photoBtn" type="submit">Upload Photos</button>
</form>
<div id="preview" class="imglist"></div>
<div id="photoStatus" class="file-info"></div>
</div>

<div style="text-align:center; margin:20px 0;">
<button id="analyzeBtn" onclick="analyzeAll()" style="background:#28a745; font-size:1.1em;">
🔍 Analyze All Uploaded Files
</button>
</div>

<div id="results"></div>
</div>

<script>
// PDF Upload
function uploadPdf(){
let f = document.getElementById('pdfFile');
let btn = document.getElementById('pdfBtn');
let status = document.getElementById('pdfStatus');
if(!f.files.length) return false;
btn.disabled = true;
status.innerHTML = "<i>Uploading PDF...</i>";
let data = new FormData();
data.append('claimpdf', f.files[0]);
fetch('/upload_pdf', { method:'POST', body:data })
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.success){
status.innerHTML = "✓ PDF uploaded successfully";
status.style.color = 'green';
}else{
status.innerHTML = "✗ " + (obj.error||"Upload failed");
status.style.color = 'red';
}
}).catch(e=>{
btn.disabled = false;
status.innerHTML = "✗ Error: "+e;
status.style.color = 'red';
});
return false;
}

// Photo Upload
document.getElementById('photoFiles').onchange = function(ev){
let preview = document.getElementById('preview');
preview.innerHTML = '';
for(let f of ev.target.files){
let img = document.createElement('img');
img.src = URL.createObjectURL(f);
preview.appendChild(img);
}
};

function uploadPhotos(){
let f = document.getElementById('photoFiles');
let btn = document.getElementById('photoBtn');
let status = document.getElementById('photoStatus');
if(f.files.length==0) return false;
btn.disabled = true;
status.innerHTML = "<i>Uploading photos...</i>";
let data = new FormData();
for(let i=0;i<f.files.length;i++) data.append('photos', f.files[i]);
fetch('/upload_photos', { method:'POST', body:data })
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.success){
status.innerHTML = `✓ ${obj.count} photos uploaded successfully`;
status.style.color = 'green';
}else{
status.innerHTML = "✗ " + (obj.error||"Upload failed");
status.style.color = 'red';
}
}).catch(e=>{
btn.disabled = false;
status.innerHTML = "✗ Error: "+e;
status.style.color = 'red';
});
return false;
}

// Analysis
function analyzeAll(){
let btn = document.getElementById('analyzeBtn');
let results = document.getElementById('results');
btn.disabled = true;
results.innerHTML = "<div style='text-align:center; padding:20px;'><i>🔄 Analyzing claim data... (this may take up to 30 seconds)</i></div>";
fetch('/analyze')
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.error){
results.innerHTML = `<div class='err'>Error: ${obj.error}</div>`;
} else {
results.innerHTML = `<div class='analysis-section'><h3>📋 Complete Analysis</h3><div>${obj.analysis}</div></div>`;
}
}).catch(e=>{
btn.disabled = false;
results.innerHTML = `<div class='err'>Error: ${e}</div>`;
});
}
</script>
</body>
</html>
"""

@app.route('/')
def main_page():
return render_template_string(COMBINED_HTML)

@app.route('/upload_pdf', methods=['POST'])
def upload_pdf():
f = request.files.get('claimpdf')
if not f or not f.filename.lower().endswith('.pdf'):
return jsonify({"error":"No PDF file uploaded."})
pdfbytes = f.read()
if len(pdfbytes) > 10*1024*1024:
return jsonify({"error":"PDF too large (10MB max)."})
fname = uuid.uuid4().hex + ".pdf"
with open(os.path.join(PDF_DIR, fname), 'wb') as out:
out.write(pdfbytes)
return jsonify({"success": True})

@app.route('/upload_photos', methods=['POST'])
def upload_photos():
files = request.files.getlist('photos')
if not files:
return jsonify({"error":"No files uploaded."})
count = 0
for f in files:
ext = os.path.splitext(f.filename)[-1].lower()
if ext not in ('.jpg','.jpeg','.png','.gif','.webp'):
continue
imgbytes = f.read()
if len(imgbytes) > 10*1024*1024:
continue
fname = uuid.uuid4().hex + ext
with open(os.path.join(IMG_DIR, fname), 'wb') as out:
out.write(imgbytes)
count += 1
if count==0:
return jsonify({"error":"No valid image files found."})
return jsonify({"success": True, "count": count})



@app.route('/analyze', methods=['GET'])
def analyze_all():
global session
try:
from google.genai.types import Part, UserContent
content = UserContent(parts=[Part(text="Analyze all available insurance claim documents and photos")])
# Collect all text output from sequential agent
all_text = ""
for event in runner.run(
user_id=session.user_id,
session_id=session.id,
new_message=content
):
for part in event.content.parts:
if part.text: # Check if text is not None
all_text += part.text + "\n"
if not all_text.strip():
return jsonify({"error": "No analysis results were generated. Please ensure files are uploaded."})
return jsonify({"analysis": all_text.strip()})
except Exception as e:
return jsonify({"error": f"Server error: {str(e)}"})

if __name__ == "__main__":
app.run(debug=True, port=5000)
간단한 웹 인터페이스를 통해 증빙 서류나 사진을 업로드한 뒤, 고객은 “업로드한 모든 파일 분석” 기능을 사용해 자신의 청구 제출물에 대한 즉각적이고 명확하며 맞춤형 피드백을 받을 수 있습니다. 내부적으로는 이 지점에서 특화된 AI 에이전트들의 오케스트레이션이 시작됩니다.
먼저 어시스턴트는 제출된 PDF(보통 공식 청구서)를 확인합니다. PDF를 읽은 뒤, 보험 전문가의 추론을 모사하는 대규모 언어 모델을 사용해 내용을 분석합니다. 이때 누락되었거나 모호하거나 애매한 부분을 찾아내고, 실제 청구 담당자가 물을 법한 후속 질문 목록을 생성해 고객이 중요한 공백을 식별하고 보완하도록 안내합니다.
각 이미지는 선명하고 명확하며 편집되지 않았는지, 그리고 상황을 파악할 수 있을 만큼의 맥락을 제공하는지(찌그러진 부분의 클로즈업만이 아니라 주변 환경까지 드러나는지) 평가됩니다. AI는 각 이미지에 대해 세부 분석을 제공하고, 해당 이미지가 청구 처리에 필요한 기준을 충족하는지 여부와 그 이유를 설명합니다.

세 번째 요약 에이전트가 두 가지 피드백 흐름을 통합해, 문서 분석과 사진 검토 결과를 하나의 읽기 쉬운 보고서로 묶습니다. 이 포괄적 요약을 통해 고객은 보험사에 제출하기 전에 어떤 증빙은 충분히 탄탄하고 어떤 부분은 보완이 필요한지 빠르게 파악할 수 있습니다.
누락된 문서, 낮은 이미지 품질, 불명확한 서술처럼 사전에 발견할 수 있는 문제를 미리 점검함으로써, 시스템은 보험 설계사가 추가 정보를 요청하느라 발생할 수 있는 지연을 예방하고 사용자가 시간을 절약하도록 돕습니다. 또한 고객이 첫 시도에 모든 필수 정보를 완비해 제출하도록 안내하여, 관련된 모든 이들에게 보험 절차를 더 쉽고, 더 명확하며, 더 효율적으로 만들어 줍니다.
워크플로가 모듈식(각 단계를 명확히 정의된 에이전트로 구성)으로 설계되어 있어 확장이 쉽습니다. 앞으로 새로운 요구 사항이나 청구 유형이 등장하더라도, 추가 특화 에이전트를 손쉽게 연결해 나머지 파이프라인에 큰 변경 없이 시스템을 최신 상태로 유지할 수 있습니다. 이러한 모듈형 설계는 매끄러운 고객 경험을 보장하고, 보험사와 고객 모두의 변화하는 요구를 지속적으로 지원합니다.

Google ADK와 함께 Weave 트레이싱 활용

이 프로젝트에서는 에이전트 워크플로의 상세 트레이스를 수집하는 주된 방법으로 OpenTelemetry(OTEL)를 Weave와 연동하여 활성화합니다. OTEL을 설정하는 과정은 다음과 같습니다 init_weave_otel_adk 함수는 모든 에이전트 호출, 도구 실행, 모델 컴플리션과 그에 대응하는 입력과 출력을 Weave 대시보드로 자동 전송할 수 있게 해줍니다. 이 통합은 각 파이프라인 실행마다 Google ADK가 내보내는 구조화된 트레이스와 스팬을 활용하여, 각 단계에서 청구가 어떻게 처리되는지에 대한 포괄적인 가시성을 제공합니다.
아래의 Weave 인터페이스 스크린샷은 파이프라인의 모든 단계가 어떻게 기록되는지를 보여줍니다. 왼쪽에는 에이전트 호출의 전체 호출 트리가 표시되며, 각 노드는 하위 에이전트 또는 도구를 나타냅니다. 에이전트 구조(청구 분석기, PDF 도구, 사진 분석기, 최종 보고서 작성기)를 확인하고, 호출 흐름을 끝까지 추적할 수 있습니다. 오른쪽에는 Weave가 각 호출에 대한 전체 요청과 모델 응답 페이로드를 표시하여, 에이전트가 내린 특정 입력/출력 결정들을 디버깅하는 데 도움을 줍니다.

OTEL의 트레이싱을 보완하기 위해, 우리는 명시적으로 호출하기도 합니다 weave.init() 그리고 사용하십시오 @weave.op 우리 코드에서 사용자 정의 도구에 데코레이터를 적용합니다. 이 방식은 해당 데코레이터로 표시된 모든 Python 함수나 도구가 @weave.op 시각적 트레이스에도 포함되어 PDF 분석이나 이미지 처리처럼 세분화된 단계를 명확히 기록하고 점검할 수 있습니다. 이러한 함수에서 전체 PDF 텍스트나 이미지 분석용 호출 ID와 같은 정보를 반환하면, Weave UI에서 해당 아티팩트와 결과를 직접 손쉽게 검토할 수 있습니다.
이미지 분석기 도구 안에 호출 ID를 기록해 두었기 때문에, 해당 트레이스를 손쉽게 검색하고 이미지 분석기의 성능을 시각화할 수 있습니다. 아래는 데모에서 사용한 호출입니다:

이런 종류의 로깅은 겉보기에는 단순해 보일 수 있지만, 그 효용은 매우 큽니다. 프로덕션 환경에서는 모델의 성능을 모니터링하고 시각화할 수 있어야 하며, 견고한 가시성이 확장 가능하고 신뢰할 수 있는 시스템의 핵심이 됩니다. 특히 이미지 분석 파이프라인에서는 데이터의 복잡성과 미묘한 엣지 케이스 가능성 때문에 문제 해결이 어려울 수 있어, 이러한 점이 더욱 중요합니다.

결론

Google의 Agent Development Kit (ADK)와 Weave + OpenTelemetry 통합의 조합은 실용적인 AI 기반 에이전트 파이프라인을 구축, 배포, 유지하는 데 강력한 기반을 제공합니다. 모듈형 에이전트 구성, 견고한 세션 및 상태 관리, 유연한 도구와 아티팩트 통합을 채택함으로써, ADK는 확장 가능한 도구와 추상화를 활용해 자연어 연구와 비평부터 정교한 문서·이미지 분석에 이르기까지 실제 문제를 해결할 수 있도록 개발자를 지원합니다.
Weave의 심층 트레이싱과 W&B 대시보드를 통한 프로덕션 지향 가시성은 실험적 코드를 신뢰할 수 있는 운영 시스템으로 끌어올립니다. 에이전트의 모든 결정, 도구 호출, 아티팩트가 자동으로 로깅되고 시각화되므로 병목이나 오류를 빠르게 파악할 수 있을 뿐 아니라, 시간에 따른 모델과 파이프라인 성능에 대한 귀중한 인사이트도 얻을 수 있습니다. 이러한 수준의 투명성은 정밀성과 신뢰성이 최우선인 보험 청구 처리와 같은 도메인에서 특히 중요합니다.
다단계 언어 파이프라인, 고객 대상 어시스턴트, 고급 이미지 검증 도구 중 무엇을 구축하든, Weave의 추적성과 모니터링으로 강화된 ADK의 에이전틱 아키텍처는 빠른 반복, 신뢰할 수 있는 디버깅, 그리고 자신 있는 확장을 가능하게 합니다. 이러한 모범 사례를 따름으로써, 지능적이고 유용할 뿐 아니라 유지 보수가 용이하고 감사를 통해 검증 가능하며 현대 프로덕션 환경의 요구에 대비된 AI 솔루션을 보장할 수 있습니다.





이 글은 AI가 번역한 기사입니다. 오역이 있을 수 있으니 댓글로 알려 주세요. 원문 보고서는 다음 링크에서 확인할 수 있습니다: 원문 보고서 보기