23 에이전트 배치

에이전트를 배치하면 에이전트가 호스트된 응용 프로그램으로 전환됩니다.

플레이그라운드 또는 다른 AI 컴퓨트에 연결된 동일한 AI 컴퓨트에 에이전트를 배포할 수 있습니다. 에이전트에 대한 최신 변경사항을 연결된 AI 컴퓨트에 배포할 때 배포된 에이전트는 배포 시 에이전트의 스냅샷을 나타냅니다. 배치된 에이전트를 최신 버전으로 업데이트하려면 에이전트를 재배치해야 합니다.

각 에이전트에는 고유한 에이전트 키에 따라 달라지는 안정적인 배포 URL이 있습니다. 에이전트를 여러 번 재배치하면 에이전트가 배치 URL 뒤에 덮어씁니다.

에이전트 배치에는 다음과 같은 제한 사항이 있습니다.
  • 에이전트는 지정된 시간에 하나의 AI 컴퓨트 클러스터에만 배치할 수 있습니다.
  • 동일한 에이전트를 동일한 AI 컴퓨트 클러스터에 여러 번 배치하면 이전에 배치된 에이전트의 반복이 겹쳐쓰여집니다.

에이전트를 배치한 후에는 채팅 URI를 검색하여 에이전트의 세부정보 탭에서 프로그래밍 방식으로 질의를 실행하고 에이전트에서 응답을 검색할 수 있습니다.


[세부사항] 탭이 열리고 강조 표시된 상태로 에이전트 페이지가 열립니다. AI 컴퓨트에 배치되고 끝점 URL이 강조 표시됩니다.

끝점 URL이 안정적이며 각 에이전트에 연결됩니다. URL에는 각 에이전트에 지정된 고유한 agentID가 포함됩니다. 즉, 에이전트의 배치를 해제하고 다시 배치하는 경우 URL은 동일하게 유지됩니다. 단점은 끝점을 호출하는 클라이언트 코드를 수정할 필요가 없다는 것입니다. 단점은 운용 중인 에이전트를 겹쳐쓸 수 있다는 것입니다.

URL의 구조는 다음과 같습니다.
https://gateway.aidp.{oci-region}.oci.oraclecloud.com/agentendpoint/{agentId}/{protocol}
설명:
  • oci-region는 AI 데이터 플랫폼 인스턴스 리전에 해당합니다.
  • agentId은 에이전트와 연관된 고유 ID입니다.
  • protocol는 통신 프로토콜입니다. chat는 OpenAI Responses API 형식을 따르고 a2a는 에이전트-에이전트 통신 프로토콜을 따릅니다. 각 에이전트 끝점에 대해 두 프로토콜을 모두 사용할 수 있습니다. 자세한 내용은 A2A 에이전트 배치를 참조하십시오.

주:

Details(세부정보) 탭에는 두 개의 AI 컴퓨트가 나열됩니다. Attached to AI Compute(AI 컴퓨트에 연결됨)는 플레이그라운드에서 에이전트를 테스트하는 데 사용됩니다. AI 컴퓨트에 배포됨은 배포된 에이전트를 호스팅합니다.

에이전트를 배치하면 끝점 URL 필드가 채워집니다. 운용 애플리케이션에서 이 끝점 URL을 호출할 수 있습니다.

에이전트 배치

생성한 에이전트를 배치하여 다른 사용자가 AI 데이터 플랫폼 인스턴스에서 보고 사용할 수 있도록 합니다.

  1. 홈 페이지에서 배치할 에이전트가 포함된 폴더로 이동합니다.
  2. 에이전트 옆에 있는 작업 3 점 아이콘 작업을 누르고 배치를 누릅니다. 에이전트 이름을 누르고 오른쪽 상단에서 배치를 누를 수도 있습니다.

    화면 오른쪽 상단에 있는 Deploy 버튼이 강조 표시된 상태로 에이전트가 열립니다.

  3. 배치된 에이전트에 연결할 AI 컴퓨트를 선택합니다.
  4. 승인 유형에 대해 AIDP 워크벤치를 선택합니다.
  5. 세션 데이터 보존 정책을 선택합니다.
    • 보유 기간의 경우 세션 데이터가 보존되는 기간(일)을 제공합니다.
    • 세션 크기 제한의 경우 세션이 도달할 수 있는 최대 크기를 제공합니다.
    • 스레드 수 제한에 대해 보존되는 최대 세션 스레드 수를 제공합니다.
  6. 배치를 누릅니다.

OAuth2로 에이전트 배치

OAuth2 인증을 사용하여 외부 ID 제공자에 연결하도록 생성 및 구성된 에이전트를 배치할 수 있습니다.

  1. 홈 페이지에서 배치할 에이전트가 포함된 폴더로 이동합니다.
  2. 에이전트 옆에 있는 작업 3 점 아이콘 작업을 누르고 배치를 누릅니다. 에이전트 이름을 누르고 오른쪽 상단에서 배치를 누를 수도 있습니다.

    화면 오른쪽 상단에 있는 Deploy 버튼이 강조 표시된 상태로 에이전트가 열립니다.

  3. 배치된 에이전트에 연결할 AI 컴퓨트를 선택합니다.
  4. 권한 부여 유형에 대해 OAuth2를 선택합니다.
  5. 대상자 청구를 제공합니다. AI 데이터 플랫폼 워크벤치는 이 필드를 자동으로 채우지만 ID 제공자의 대상자 클레임으로 대체할 수 있습니다.
  6. JWKS를 검색할 발행자 클레임URI를 제공합니다. 이 정보는 ID 제공자에서 파생됩니다.
  7. 세션 데이터 보존 정책을 선택합니다.
  8. 배치를 누릅니다.

에이전트 배치 해제

MANAGE 권한이 있는 에이전트를 사용할 수 없도록 배치 해제하도록 선택할 수 있습니다.

  1. 홈 페이지에서 배치를 해제할 에이전트가 포함된 폴더로 이동합니다.
  2. 에이전트 옆에 있는 작업 3 점 아이콘 작업을 누르고 배치 해제를 누릅니다.

    [배치 해제] 단추가 강조 표시된 에이전트 상단의 자른 이미지

  3. 배치 해제를 누릅니다.

A2A 에이전트 배치

Agent2Agent(A2A) 프로토콜은 서로 다른 프레임워크로 구축된 에이전트, 서로 다른 공급업체에서 호스팅하거나 불투명한 원격 시스템으로 실행되는 에이전트를 포함하여 독립적인 AI 에이전트 간의 통신을 위한 개방형 표준입니다.

이 회사의 목적은 이러한 에이전트가 서로의 기능을 검색하고, 지원되는 입/출력 형식을 협상하고, 작업을 위임 또는 협업하고, 내부 메모리, 도구 또는 구현 세부 정보를 노출하지 않고도 정보를 안전하게 교환할 수 있도록 공유 상호 작용 모델을 제공하는 것입니다. 자세한 내용은 Agent2Agent (A2A) Protocol을 참조하십시오.

A2A는 에이전트 상호 운용성을 해결하기 위한 것입니다. 즉, 모든 에이전트 통합이 사용자 정의되는 대신 클라이언트 또는 다른 에이전트가 공통 개념 및 작업 세트를 사용하여 A2A 호환 원격 에이전트와 상호 작용할 수 있습니다. 사양은 메시지, 작업, 부품, 아티팩트, 스트리밍 업데이트 및 푸시 알림을 중심으로 하며 동기 회신, 장기 실행 비동기 작업, 스트리밍 및 엔터프라이즈 스타일의 인증/보안 패턴을 지원합니다.

Oracle AI Data Platform에서는 배포된 모든 에이전트에 A2A 클라이언트 애플리케이션에서 호출할 수 있는 /A2A 호출 경로가 제공됩니다.

에이전트 카드란 무엇입니까?

에이전트 카드는 A2A 서버에서 게시한 JSON 메타데이터 문서입니다. AIDP에서 A2A 서버는 에이전트 배치를 호스팅하는 AI 컴퓨트입니다.

이 카드는 에이전트의 ID, 서비스 끝점, 지원되는 프로토콜/전송, 기능, 기술, 지원되는 입/출력 모드 및 인증 요구 사항에 대해 설명합니다. 클라이언트는 이 카드를 사용하여 에이전트가 적합한지 여부 및 에이전트의 호출 방법을 파악합니다. 제대로 문서화된 에이전트 카드는 A2A 프로토콜의 요구 사항입니다.

AI 데이터 플랫폼 워크벤치의 에이전트 카드는 초안 상태입니다. 즉, 에이전트가 배치되지 않았거나 게시됨입니다. 즉, 카드가 에이전트와 함께 배치되었습니다.

에이전트 카드 작업

에이전트를 개발하는 동안 에이전트의 작업 메뉴에서 카드를 사용할 수 있습니다.

다음 두 에이전트 카드에 액세스할 수 있습니다.
  • 초안 카드는 개발 중인 에이전트의 현재 상태를 반영합니다.
  • 게시된 카드는 에이전트가 배포될 때 찍은 카드의 스냅샷에 해당합니다. 게시된 카드는 배치된 에이전트의 상태를 반영합니다.

에이전트 카드 필드

AI Data Platform Workbench는 현재 A2A 프로토콜 에이전트 카드 필드의 하위 세트를 지원합니다(여기서는 A2A 프로토콜 - 에이전트 카드).

필드 필수사항 설명
name 에이전트에 대한 사람이 읽을 수 있는 이름입니다. 예: "레시피 에이전트"
description 에이전트에 대한 사람이 읽을 수 있는 설명으로, 사용자와 다른 에이전트가 해당 목적을 이해할 수 있도록 지원합니다. 예: "사용자에게 조리법 및 요리를 제공하는 데 도움이 되는 에이전트입니다."
Agent Version 에이전트의 버전입니다. 예: "1.0.0"
Documentation URL 아니요 에이전트에 대한 추가 설명서를 제공하는 URL입니다.
Provider - Organization 아니요 에이전트의 서비스 제공자입니다.
Provider - URL 아니요 서비스 제공자의 URL입니다.
Capabilities 에이전트에서 지원하는 A2A 기능 세트입니다.

streaming만 구성할 수 있음(True/False)

Skills 기술은 에이전트의 능력을 나타냅니다. 그것은 대체로 설명적인 개념이지만 에이전트가 성공할 가능성이 더 큰 일련의 행동을 나타냅니다. 기술은 AgentSkill의 배열을 나타냅니다.

각 AgentSkill은 에이전트의 기능을 문서화하는 여러 필드로 구성됩니다. 에이전트 카드에서 에이전트 기술 정의는 시간이 가장 많이 걸리는 작업이며 반복 프로세스입니다. 배포 전에 초안 에이전트 카드에서 기술(나머지 에이전트 카드와 함께)을 편집할 수 있습니다.

주:

inputModes, outputModes securityRequirements는 AI 데이터 플랫폼 워크벤치에서 제공되며 수정할 수 없습니다.
필드 필수사항 설명
Skill ID 에이전트 스킬에 대한 고유 식별자입니다.
Skill Name 스킬에 대한 사람이 읽을 수 있는 이름입니다.
Description 스킬에 대한 자세한 설명입니다.
Tags 스킬의 기능을 설명하는 키워드 세트입니다.
Examples 아니요 이 스킬이 처리할 수 있는 프롬프트 또는 시나리오의 예입니다.

에이전트 배치 끝점 A2A 경로

/a2a 경로는 /chat 외에도 배포된 에이전트의 URL에 표시됩니다.

예를 들어, 에이전트는 다음 경로를 외부 클라이언트에 노출합니다.

  • https://gateway.aidp.{oci-region}.oci.oraclecloud.com/agentendpoint/{agentId}/chat
  • https://gateway.aidp.{oci-region}.oci.oraclecloud.com/agentendpoint/{agentId}/a2a

두 경로(/chat, /a2a) 모두 별도의 클라이언트에서 사용할 수 있습니다.

A2A의 세션 변수

세션 변수의 값은 메시지 metadata 필드에서 A2A 에이전트로 전달할 수 있습니다. 아래 JSON 코드 조각은 세 개의 세션 변수(userName, geoLocationos)가 있는 a2a 에이전트에 발행된 사용자 메시지의 페이로드를 보여줍니다.

{
  "jsonrpc": "2.0",
  "method": "message/send",
  "params": {
    "contextId": "session_12345",
    "taskId": "task_67890",
    "message": {
      "role": "user",
      "parts": [
        {
          "text": "What is the current status of my order?",
        }
      ],
      "metadata": {
        "sessionvariables.userName": "George",
        "sessionvariables.geoLocation": “Dallas, TX”,
        "sessionvariables.os": "mobile_ios"
      }
    }
  },
  "id": "rpc-99821"
}

예: OCI CLI를 사용하여 A2A 에이전트 호출(비스트리밍)

oci raw-request \
  --http-method POST \
  --auth security_token \
  --request-body '{
    "id": "<your-request-id>",
    "jsonrpc": "2.0",
    "method": "message/send",
    "params": {
      "configuration": {
        "acceptedOutputModes": [
          "text/plain",
          "text"
        ]
      },
      "message": {
        "contextId": "<your-context-id>",
        "kind": "message",
        "messageId": "<your-message-id>",
        "parts": [
          {
            "kind": "text",
            "text": "What is the capital of India?"
          }
        ],
        "role": "user"
      }
    }
  }' \
  --request-headers '{
    "x-session-id": "<your-session-id>",
    "dh-user-principal": "<your-user-principal>"
  }' \
  --target-uri " <your-a2a-agent-endpoint-url>"

예: OCI CLI를 사용하여 A2A 에이전트 호출(스트리밍)

oci raw-request \
  --http-method POST \
  --auth security_token \
  --request-body '{
    "id": "<your-request-id>",
    "jsonrpc": "2.0",
    "method": "message/stream",
    "params": {
      "configuration": {
        "acceptedOutputModes": [
          "text/plain",
          "text"
        ]
      },
      "message": {
        "contextId": "<your-context-id>",
        "kind": "message",
        "messageId": " <your-message-id>",
        "parts": [
          {
            "kind": "text",
            "text": "What is the capital of India?"
          }
        ],
        "role": "user"
      }
    }
  }' \
  --request-headers '{
    "x-session-id": "<your-session-id>",
    "dh-user-principal": "<user-principal>"
  }' \
  --target-uri "<your-a2a-agent-endpoint-url>"

예: A2A 클라이언트 SDK

import asyncio
import json
import logging
import typing
from collections.abc import Iterator
import uuid
import httpx
import oci
from a2a.client import A2AClient, ClientFactory
from a2a.types import (
    AgentCard,
    Message,
    Part,
    Role,
    TextPart,
    SendMessageRequest,
    MessageSendParams,
    MessageSendConfiguration,
    Task, SendMessageSuccessResponse, SendStreamingMessageRequest,
)

class OCIAuth(httpx.Auth):
    """httpx auth implementation using OCI signer via requests auth adapter."""

    def __init__(self, signer: oci.signer.AbstractBaseSigner):
        self._requests_auth = _OCIRequestsAuth(signer)

    def auth_flow(self, request: httpx.Request) -> Iterator[httpx.Request]:
        req = RequestsRequest(
            method=request.method,
            url=str(request.url),
            headers=dict(request.headers),
            data=request.content,
        )
        prepared: RequestsPreparedRequest = req.prepare()
        prepared = self._requests_auth(prepared)
        request.headers.update(dict(prepared.headers))
        yield request



def getOCIAuth():
    conf = oci.config.from_file(profile_name="DEFAULT")
    token_file = conf['security_token_file']
    token = None
    with open(token_file, 'r') as f:
        token = f.read()
    private_key = oci.signer.load_private_key_from_file(conf['key_file'])
    signer = oci.auth.signers.SecurityTokenSigner(token, private_key)
    auth = OCIAuth(signer=signer)
    return auth


async def _call_agent_with_a2a(agent_url: str, query: str, context_id: str,auth:OCIAuth) -> str:
    """Call an agent using the A2A protocol."""
    try:
        # Initialize OCI signer
        #headers = {"dh-user-principal": "dh-user"}
        headers = {"Accept": "*/*",
                   "dh-user-principal": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

        async with httpx.AsyncClient(timeout=60.0, auth=auth,headers=headers) as hc:
            agent_card = await _get_agent_card(agent_url,auth)
            print(f"Agent card is {agent_card}")

            client =  A2AClient(httpx_client=hc, agent_card=agent_card)
            # Create message
            message = Message(
                message_id=str(uuid.uuid4()),
                context_id=context_id,
                role=Role.user,
                parts=[Part(root=TextPart(text=query))],
                metadata={"sessionvariables.cred.mcp.weatherReportMCP.bearer": "valid-123"}
            )
            request = SendMessageRequest(
                id=str(uuid.uuid4()),  # Add the required id field
                params=MessageSendParams(
                    message=message,
                    configuration=MessageSendConfiguration(acceptedOutputModes=["text/plain", "text"]),
                ),
            )
            #json_string = json.dumps(message, indent=4)
            print(f"Send request : {request}")

            response = await client.send_message(request)

            logging.info("Received response from A2A server: %s", response.root.result)
            # Extract response
            result = response.root.result
            # Handle different response types


            if isinstance(result, Task):
                # Task response
                if result.artifacts:
                    # Extract text from artifacts
                    texts = []
                    for artifact in result.artifacts:
                        for part in artifact.parts:
                            if hasattr(part, "root") and hasattr(part.root, "text"):
                                texts.append(part.root.text)
                    return "\n".join(texts) if texts else "Task completed with no text response"
                elif result.status and result.status.message:
                    logging.info(f"Received Task status {result.status.state} from A2A server and status message is {result.status.message}", result.status.message)
                    if  result.status.state== "failed":
                        print("Failure observed in Task invocation")
                        for m_part in result.status.message.parts:
                           print(f"Error message  { m_part.root.text}")

                    return get_message_text(result.status.message)
                else:
                    return f"Task {result.id} status: {result.status.state if result.status else 'unknown'}"

            elif isinstance(result, Message):
                return get_message_text(result)
            else:
                logging.warning(f"Unexpected response type: {type(result)}")
                return "Received response but unable to extract text"
    except Exception as ex:
        logging.error(f"Error calling agent at {agent_url}: {ex}", exc_info=True)
        return f"Error communicating with agent: {str(ex)}"


async def _call_agent_with_a2a_with_stream(agent_url: str, query: str, context_id: str, auth: OCIAuth) -> str:
    """Call an agent using the A2A protocol with streaming (SSE) and return the final artifact text."""
    try:
        async with httpx.AsyncClient(timeout=60.0, auth=auth) as hc:
            agent_card = await _get_agent_card(agent_url)
            if not agent_card:
                return "No Agent Card Found"
            print(f"Agent card is {agent_card}")

            client = A2AClient(httpx_client=hc, agent_card=agent_card)
            message = Message(
                message_id=str(uuid.uuid4()),
                context_id=context_id,
                role=Role.user,
                parts=[Part(root=TextPart(text=query))],
            )
            request = SendStreamingMessageRequest(
                id=str(uuid.uuid4()),
                params=MessageSendParams(
                    message=message,
                    configuration=MessageSendConfiguration(acceptedOutputModes=["text/plain", "text"]),
                ),
            )
            print("Invoking Remote Agent request (beautified JSON):")
            print(json.dumps(request.model_dump(), indent=2, ensure_ascii=False))
            # Expected event types:
            # - TaskStatusUpdateEvent (working/in-progress)
            # - TaskArtifactUpdateEvent (contains Artifact.parts[].root.text) -> final output
            final_artifact_text_parts: list[str] = []

            async for event in client.send_message_streaming(request):
                # Print each SSE event as-is (SDK object)
                print(f"[A2A stream event] {event}")

                try:
                    result = getattr(event.root, "result", None)
                    if not result:
                        continue

                    # TaskArtifactUpdateEvent and TaskStatusUpdateEvent are SDK types; to avoid tight coupling,
                    # extract by attribute presence.
                    artifact = getattr(result, "artifact", None)
                    if artifact and getattr(artifact, "parts", None):
                        for part in artifact.parts:
                            root = getattr(part, "root", None)
                            txt = getattr(root, "text", None)
                            if txt:
                                final_artifact_text_parts.append(txt)
                except Exception:
                    # Keep streaming even if an event can't be parsed
                    continue

            return "\n".join([t for t in final_artifact_text_parts if t]).strip() or "Stream completed (no artifact text)."
    except Exception as ex:
        logging.error(f"Error calling agent at {agent_url}: {ex}", exc_info=True)
        return f"Error communicating with agent: {str(ex)}"

초안 에이전트 카드 편집

아직 배포되지 않은 에이전트의 에이전트 카드를 편집할 수 있습니다.

  1. 에이전트로 이동합니다.
  2. 작업을 누른 다음 에이전트 카드 보기에이전트 카드 초안을 누릅니다.

    에이전트 플로우 시각적 빌더가 표시됩니다. 작업 메뉴 및 에이전트 카드 보기 하위 옵션이 선택되어 있습니다. 초안 에이전트 카드가 강조 표시됩니다.

  3. 편집을 누릅니다.

    초안 에이전트 카드 대화 상자가 표시됩니다. Edit 버튼이 강조 표시됩니다.

  4. 오른쪽 상단에서 양식 또는 JSON을 눌러 뷰를 전환할 수 있습니다. JSON 뷰는 더 완전하지만 읽기 전용입니다. 양식 보기의 필드만 편집할 수 있습니다.

    에이전트 카드 편집 대화 상자가 표시됩니다. JSON 및 양식 보기 아이콘이 강조 표시됩니다. JSON 뷰가 선택되었습니다.

  5. 필요에 따라 필드를 수정합니다.
  6. 스킬 추가를 눌러 A2A 클라이언트에 표시할 스킬을 추가합니다.

    에이전트 카드 편집 대화 상자가 표시됩니다. 스킬 추가 버튼이 강조 표시됩니다.

  7. 저장을 누릅니다.

게시된 에이전트 카드 편집

에이전트를 배치 해제하거나 재배치하지 않고 게시된 에이전트 카드를 수정할 수 있습니다.

게시된 카드는 배포 시 초안 카드의 스냅샷에 해당합니다.

주:

게시된 카드에 대한 변경 사항은 즉시 A2A 클라이언트가 액세스할 수 있는 agent-card.json 파일에 반영됩니다.
  1. 에이전트로 이동합니다.
  2. 작업을 누른 다음 에이전트 카드 보기게시된 에이전트 카드를 누릅니다.

    Visual Builder 캔버스가 표시됩니다. 작업 메뉴 및 에이전트 카드 보기 하위 옵션이 선택되어 있습니다. 게시된 에이전트 카드가 강조 표시됩니다.

  3. 오른쪽 상단에서 양식 또는 JSON을 눌러 뷰를 전환할 수 있습니다. JSON 뷰는 더 완전하지만 읽기 전용입니다. 양식 보기의 필드만 편집할 수 있습니다.
  4. 편집을 누릅니다.

    게시된 에이전트 카드 대화 상자가 표시됩니다. Edit 버튼이 강조 표시됩니다.

  5. 필요에 따라 필드를 수정합니다.
  6. 스킬 추가를 눌러 A2A 클라이언트에 표시할 스킬을 추가합니다.

    에이전트 카드 편집 대화상자 "이 카드는 라이브 에이전트에 연결되어 있고, 업데이트는 배치된 에이전트 카드에 영향을 줍니다." 및 Publish changes(변경사항 게시) 버튼이 강조 표시되어 있습니다.

  7. 변경사항 게시를 누릅니다.
게시된 에이전트 카드는 공개적으로 사용할 수 없습니다. agent-card.json의 내용을 검사하려면 사용자를 인증하고 올바른 권한(READ)이 있어야 합니다. 다른 에이전트는 표준 경로에서 HTTP GET 요청을 통해 에이전트 기능 및 메타데이터를 검색할 수 있습니다.
https://gateway.aidp.{oci-region}.oci.oraclecloud.com/agentendpoint/{agentId}/a2a/agent-card.json