Gemini और Temporal की मदद से, भरोसेमंद एआई एजेंट बनाना

इस ट्यूटोरियल में, ReAct-स्टाइल वाला एजेंटिक लूप बनाने का तरीका बताया गया है. यह लूप, तर्क करने के लिए Gemini API का इस्तेमाल करता है. साथ ही, Temporal का इस्तेमाल करके, लंबे समय तक काम करता है. इस ट्यूटोरियल का पूरा सोर्स कोड GitHub पर उपलब्ध है.

एजेंट, मौसम की सूचनाएँ देखने या आईपी पते की जियोलोकेशन का पता लगाने जैसे टूल का इस्तेमाल कर सकता है. जब तक एजेंट के पास जवाब देने के लिए ज़रूरी जानकारी नहीं होती, तब तक वह लूप में रहेगा.

इस डेमो को किसी सामान्य एजेंट डेमो से अलग बनाने वाली बात यह है कि यह लंबे समय तक काम करता है. हर एलएलएम कॉल, हर टूल इनवोकेशन, और एजेंटिक लूप के हर चरण को Temporal सेव करता है. अगर प्रोसेस क्रैश हो जाती है, नेटवर्क बंद हो जाता है या एपीआई का समय खत्म हो जाता है, तो Temporal अपने-आप फिर से कोशिश करता है और पिछली बार पूरे किए गए चरण से प्रोसेस को फिर से शुरू करता है. बातचीत का इतिहास नहीं मिटता और टूल कॉल को गलत तरीके से दोहराया नहीं जाता.

आर्किटेक्चर

आर्किटेक्चर में तीन हिस्से होते हैं:

  • वर्कफ़्लो: यह एक एजेंटिक लूप होता है, जो टास्क पूरा करने के लॉजिक को व्यवस्थित करता है.
  • गतिविधियां: काम की अलग-अलग यूनिट (एलएलएम कॉल, टूल कॉल), जिन्हें Temporal लंबे समय तक बनाए रखता है.
  • वर्कर: यह एक ऐसी प्रोसेस है जो वर्कफ़्लो और गतिविधियों को पूरा करती है.

इस उदाहरण में, इन तीनों को एक ही फ़ाइल (durable_agent_worker.py) में रखा जाएगा. हालांकि, असल दुनिया में इन्हें अलग-अलग रखा जाता है, ताकि इन्हें अलग-अलग तरीके से डिप्लॉय किया जा सके और इनकी क्षमता को बढ़ाया जा सके. आपको एजेंट को प्रॉम्प्ट देने वाला कोड, दूसरी फ़ाइल (start_workflow.py) में रखना होगा.

ज़रूरी शर्तें

इस गाइड को पूरा करने के लिए, आपको इनकी ज़रूरत होगी:

  • Gemini API पासकोड. Google AI Studio में जाकर, इसे बिना किसी शुल्क के बनाया जा सकता है.
  • Python का 3.10 या इसके बाद का वर्शन.
  • लोकल डेवलपमेंट सर्वर चलाने के लिए, Temporal CLI.

सेटअप

शुरू करने से पहले, पक्का करें कि आपके पास स्थानीय तौर पर चल रहा Temporal डेवलपमेंट सर्वर हो:

temporal server start-dev

इसके बाद, ज़रूरी डिपेंडेंसी इंस्टॉल करें:

pip install temporalio google-genai httpx pydantic python-dotenv

अपने प्रोजेक्ट की डायरेक्ट्री में, Gemini API पासकोड के साथ .env फ़ाइल बनाएं. Google AI Studio से एपीआई पासकोड पाया जा सकता है.

echo "GOOGLE_API_KEY=your-api-key-here" > .env

लागू करना

इस ट्यूटोरियल के बाकी हिस्से में, durable_agent_worker.py के बारे में पूरी जानकारी दी गई है. इसमें एजेंट को एक-एक करके बनाया गया है. फ़ाइल बनाएं और साथ-साथ निर्देशों का पालन करें.

इंपोर्ट और सैंडबॉक्स सेटअप करना

उन इंपोर्ट से शुरू करें जिन्हें पहले से तय किया जाना चाहिए. workflow.unsafe.imports_passed_through() ब्लॉक, Temporal के वर्कफ़्लो सैंडबॉक्स को यह बताता है कि कुछ मॉड्यूल को बिना किसी पाबंदी के पास होने दिया जाए. यह ज़रूरी है, क्योंकि कई लाइब्रेरी (खास तौर पर httpx, जो urllib.request.Request की सबक्लास है) ऐसे पैटर्न का इस्तेमाल करती हैं जिन्हें सैंडबॉक्स ब्लॉक कर देता है.

from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    import pydantic_core  # noqa: F401
    import annotated_types  # noqa: F401

    import httpx
    from pydantic import BaseModel, Field
    from google import genai
    from google.genai import types

सिस्टम के निर्देश

इसके बाद, एजेंट की पर्सनैलिटी तय करें. सिस्टम के निर्देशों से मॉडल को यह पता चलता है कि उसे कैसे काम करना है. इस एजेंट को यह निर्देश दिया गया है कि जब किसी टूल की ज़रूरत न हो, तो वह हाइकू में जवाब दे.

SYSTEM_INSTRUCTIONS = """
You are a helpful agent that can use tools to help the user.
You will be given an input from the user and a list of tools to use.
You may or may not need to use the tools to satisfy the user ask.
If no tools are needed, respond in haikus.
"""

टूल की परिभाषाएं

अब उन टूल के बारे में बताएं जिनका इस्तेमाल एजेंट कर सकता है. हर टूल एक एसिंक फ़ंक्शन होता है, जिसमें जानकारी देने वाली डॉकस्ट्रिंग होती है. पैरामीटर लेने वाले टूल, Pydantic मॉडल का इस्तेमाल अपने सिंगल आर्ग्युमेंट के तौर पर करते हैं. यह टेंपोरल का सबसे सही तरीका है. इससे समय के साथ वैकल्पिक फ़ील्ड जोड़ने पर भी, गतिविधि के सिग्नेचर स्थिर रहते हैं.

import json

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


class GetWeatherAlertsRequest(BaseModel):
    """Request model for getting weather alerts."""

    state: str = Field(description="Two-letter US state code (e.g. CA, NY)")


async def get_weather_alerts(request: GetWeatherAlertsRequest) -> str:
    """Get weather alerts for a US state.

    Args:
        request: The request object containing:
            - state: Two-letter US state code (e.g. CA, NY)
    """
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    url = f"{NWS_API_BASE}/alerts/active/area/{request.state}"

    async with httpx.AsyncClient() as client:
        response = await client.get(url, headers=headers, timeout=5.0)
        response.raise_for_status()
        return json.dumps(response.json())

इसके बाद, आईपी पते के हिसाब से भौगोलिक जगह की जानकारी का पता लगाने वाले टूल तय करें:

class GetLocationRequest(BaseModel):
    """Request model for getting location info from an IP address."""

    ipaddress: str = Field(description="An IP address")


async def get_ip_address() -> str:
    """Get the public IP address of the current machine."""
    async with httpx.AsyncClient() as client:
        response = await client.get("https://icanhazip.com")
        response.raise_for_status()
        return response.text.strip()


async def get_location_info(request: GetLocationRequest) -> str:
    """Get the location information for an IP address including city, state, and country.

    Args:
        request: The request object containing:
            - ipaddress: An IP address to look up
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(f"http://ip-api.com/json/{request.ipaddress}")
        response.raise_for_status()
        result = response.json()
        return f"{result['city']}, {result['regionName']}, {result['country']}"

टूल रजिस्ट्री

इसके बाद, एक ऐसी रजिस्ट्री बनाएं जो टूल के नामों को हैंडलर फ़ंक्शन से मैप करती हो. get_tools() फ़ंक्शन, FunctionDeclaration.from_callable_with_api_option() का इस्तेमाल करके कॉल करने लायक ऑब्जेक्ट से, Gemini के साथ काम करने वाले FunctionDeclaration ऑब्जेक्ट जनरेट करता है.

from typing import Any, Awaitable, Callable

ToolHandler = Callable[..., Awaitable[Any]]


def get_handler(tool_name: str) -> ToolHandler:
    """Get the handler function for a given tool name."""
    if tool_name == "get_location_info":
        return get_location_info
    if tool_name == "get_ip_address":
        return get_ip_address
    if tool_name == "get_weather_alerts":
        return get_weather_alerts
    raise ValueError(f"Unknown tool name: {tool_name}")


def get_tools() -> types.Tool:
    """Get the Tool object containing all available function declarations.

    Uses FunctionDeclaration.from_callable_with_api_option() from the Google GenAI SDK
    to generate tool definitions from the handler functions.
    """
    return types.Tool(
        function_declarations=[
            types.FunctionDeclaration.from_callable_with_api_option(
                callable=get_weather_alerts, api_option="GEMINI_API"
            ),
            types.FunctionDeclaration.from_callable_with_api_option(
                callable=get_location_info, api_option="GEMINI_API"
            ),
            types.FunctionDeclaration.from_callable_with_api_option(
                callable=get_ip_address, api_option="GEMINI_API"
            ),
        ]
    )

एलएलएम से जुड़ी गतिविधि

अब Gemini API को कॉल करने वाली गतिविधि तय करें. GeminiChatRequest और GeminiChatResponse डेटाक्लास, कॉन्ट्रैक्ट तय करते हैं.

इससे फ़ंक्शन को अपने-आप कॉल करने की सुविधा बंद हो जाएगी. इसलिए, एलएलएम इनवोकेशन और टूल इनवोकेशन को अलग-अलग टास्क के तौर पर हैंडल किया जाएगा. इससे आपके एजेंट को ज़्यादा समय तक इस्तेमाल किया जा सकेगा. आपको एसडीके में पहले से मौजूद फिर से कोशिश करने की सुविधा (attempts=1) भी बंद करनी होगी, क्योंकि Temporal, फिर से कोशिश करने की सुविधा को लंबे समय तक बनाए रखता है.

import os
from dataclasses import dataclass

from temporalio import activity

@dataclass
class GeminiChatRequest:
    """Request parameters for a Gemini chat completion."""

    model: str
    system_instruction: str
    contents: list[types.Content]
    tools: list[types.Tool]


@dataclass
class GeminiChatResponse:
    """Response from a Gemini chat completion."""

    text: str | None
    function_calls: list[dict[str, Any]]
    raw_parts: list[types.Part]


@activity.defn
async def generate_content(request: GeminiChatRequest) -> GeminiChatResponse:
    """Execute a Gemini chat completion with tool support."""
    api_key = os.environ.get("GOOGLE_API_KEY")
    if not api_key:
        raise ValueError("GOOGLE_API_KEY environment variable is not set")
    client = genai.Client(
        api_key=api_key,
        http_options=types.HttpOptions(
            retry_options=types.HttpRetryOptions(attempts=1),
        ),
    )

    config = types.GenerateContentConfig(
        system_instruction=request.system_instruction,
        tools=request.tools,
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    )

    response = await client.aio.models.generate_content(
        model=request.model,
        contents=request.contents,
        config=config,
    )

    function_calls = []
    raw_parts = []
    text_parts = []

    if response.candidates and response.candidates[0].content:
        for part in response.candidates[0].content.parts:
            raw_parts.append(part)
            if part.function_call:
                function_calls.append(
                    {
                        "name": part.function_call.name,
                        "args": dict(part.function_call.args) if part.function_call.args else {},
                    }
                )
            elif part.text:
                text_parts.append(part.text)

    text = "".join(text_parts) if text_parts and not function_calls else None

    return GeminiChatResponse(
        text=text,
        function_calls=function_calls,
        raw_parts=raw_parts,
    )

डाइनैमिक टूल की गतिविधि

इसके बाद, टूल को लागू करने वाली गतिविधि तय करें. यह Temporal की डाइनैमिक ऐक्टिविटी सुविधा का इस्तेमाल करता है: टूल हैंडलर (कॉल किया जा सकने वाला) को get_handler फ़ंक्शन के ज़रिए, टूल रजिस्ट्री से हासिल किया जाता है. इससे अलग-अलग एजेंट को आसानी से तय किया जा सकता है. इसके लिए, टूल और सिस्टम के निर्देशों का अलग सेट उपलब्ध कराना होता है. एजेंटिक लूप को लागू करने वाले वर्कफ़्लो में कोई बदलाव करने की ज़रूरत नहीं होती.

यह गतिविधि, हैंडलर के सिग्नेचर की जांच करती है, ताकि यह तय किया जा सके कि आर्ग्युमेंट कैसे पास किए जाएं. अगर हैंडलर को Pydantic मॉडल की ज़रूरत होती है, तो यह Gemini के बनाए गए नेस्ट किए गए आउटपुट फ़ॉर्मैट को हैंडल करता है. उदाहरण के लिए, फ़्लैट {"state": "CA"} के बजाय {"request": {"state": "CA"}}.

import inspect
from collections.abc import Sequence

from temporalio.common import RawValue

@activity.defn(dynamic=True)
async def dynamic_tool_activity(args: Sequence[RawValue]) -> dict:
    """Execute a tool dynamically based on the activity name."""
    tool_name = activity.info().activity_type
    tool_args = activity.payload_converter().from_payload(args[0].payload, dict)
    activity.logger.info(f"Running dynamic tool '{tool_name}' with args: {tool_args}")

    handler = get_handler(tool_name)

    if not inspect.iscoroutinefunction(handler):
        raise TypeError("Tool handler must be async (awaitable).")

    sig = inspect.signature(handler)
    params = list(sig.parameters.values())

    if len(params) == 0:
        result = await handler()
    else:
        param = params[0]
        param_name = param.name
        ann = param.annotation

        if isinstance(ann, type) and issubclass(ann, BaseModel):
            nested_args = tool_args.get(param_name, tool_args)
            result = await handler(ann(**nested_args))
        else:
            result = await handler(**tool_args)

    activity.logger.info(f"Tool '{tool_name}' result: {result}")
    return result

एजेंटिक लूप वर्कफ़्लो

अब आपके पास एजेंट बनाने के लिए सभी ज़रूरी चीज़ें हैं. AgentWorkflow क्लास, एजेंट लूप वाले वर्कफ़्लो को लागू करती है. इस लूप में, गतिविधि के ज़रिए LLM को चालू किया जाता है, ताकि यह लंबे समय तक काम कर सके. इसके बाद, आउटपुट की जाँच की जाती है. अगर LLM ने किसी टूल को चुना है, तो उसे dynamic_tool_activity के ज़रिए चालू किया जाता है.

इस सामान्य ReAct स्टाइल वाले एजेंट में, जब एलएलएम किसी टूल का इस्तेमाल नहीं करता है, तो लूप पूरा हो जाता है और एलएलएम का फ़ाइनल नतीजा वापस मिल जाता है.

from datetime import timedelta

@workflow.defn
class AgentWorkflow:
    """Agentic loop workflow that uses Gemini for LLM calls and executes tools."""

    @workflow.run
    async def run(self, input: str) -> str:
        contents: list[types.Content] = [
            types.Content(role="user", parts=[types.Part.from_text(text=input)])
        ]

        tools = [get_tools()]

        while True:
            result = await workflow.execute_activity(
                generate_content,
                GeminiChatRequest(
                    model="gemini-3-flash-preview",
                    system_instruction=SYSTEM_INSTRUCTIONS,
                    contents=contents,
                    tools=tools,
                ),
                start_to_close_timeout=timedelta(seconds=60),
            )

            if result.function_calls:
                # Sending the complete raw_parts here ensures Gemini 3 thought
                # signatures are propagated correctly.
                contents.append(types.Content(role="model", parts=result.raw_parts))

                for function_call in result.function_calls:
                    tool_result = await self._handle_function_call(function_call)

                    contents.append(
                        types.Content(
                            role="user",
                            parts=[
                                types.Part.from_function_response(
                                    name=function_call["name"],
                                    response={"result": tool_result},
                                )
                            ],
                        )
                    )
            else:
                return result.text
            # Leave this in place. You will un-comment it during a durability
            # test later on.
            # await asyncio.sleep(10)

    async def _handle_function_call(self, function_call: dict) -> str:
        """Execute a tool via dynamic activity and return the result."""
        tool_name = function_call["name"]
        tool_args = function_call.get("args", {})

        result = await workflow.execute_activity(
            tool_name,
            tool_args,
            start_to_close_timeout=timedelta(seconds=30),
        )

        return result

एजेंटिक लूप पूरी तरह से टिकाऊ होता है. अगर लूप के कई इटरेशन के बाद एजेंट वर्कर क्रैश हो जाता है, तो Temporal ठीक उसी जगह से काम करना शुरू कर देगा जहां उसने छोड़ा था. इसके लिए, पहले से लागू किए गए एलएलएम इनवोकेशन या टूल कॉल को फिर से लागू करने की ज़रूरत नहीं होगी.

वर्कर स्टार्टअप

आखिर में, सभी डिवाइसों को एक साथ कनेक्ट करें. कोड, ज़रूरी कारोबारी लॉजिक को इस तरह से लागू करता है कि यह एक ही प्रोसेस में चलता हुआ दिखता है. हालांकि, Temporal का इस्तेमाल करने से यह इवेंट-ड्रिवन सिस्टम (खास तौर पर, इवेंट-सोर्स) बन जाता है. इसमें वर्कफ़्लो और गतिविधियों के बीच कम्यूनिकेशन, Temporal की ओर से उपलब्ध कराई गई मैसेजिंग के ज़रिए होता है.

टेम्पोरल वर्कर, टेम्पोरल सेवा से कनेक्ट होता है और वर्कफ़्लो और गतिविधि टास्क के लिए शेड्यूलर के तौर पर काम करता है. वर्कर, वर्कफ़्लो और दोनों गतिविधियों को रजिस्टर करता है. इसके बाद, टास्क के लिए सुनना शुरू करता है.

import asyncio
from concurrent.futures import ThreadPoolExecutor

from dotenv import load_dotenv
from temporalio.client import Client
from temporalio.contrib.pydantic import pydantic_data_converter
from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker

async def main():
    config = ClientConfig.load_client_connect_config()
    config.setdefault("target_host", "localhost:7233")
    client = await Client.connect(
        **config,
        data_converter=pydantic_data_converter,
    )

    worker = Worker(
        client,
        task_queue="gemini-agent-python-task-queue",
        workflows=[
            AgentWorkflow,
        ],
        activities=[
            generate_content,
            dynamic_tool_activity,
        ],
        activity_executor=ThreadPoolExecutor(max_workers=10),
    )
    await worker.run()


if __name__ == "__main__":
    load_dotenv()
    asyncio.run(main())

क्लाइंट स्क्रिप्ट

क्लाइंट स्क्रिप्ट (start_workflow.py) बनाएं. यह क्वेरी सबमिट करती है और नतीजे का इंतज़ार करती है. ध्यान दें कि यह उसी टास्क क्यू से कनेक्ट होता है जिसका रेफ़रंस एजेंट वर्कर में दिया गया है. start_workflow स्क्रिप्ट, उपयोगकर्ता के प्रॉम्प्ट के साथ वर्कफ़्लो टास्क को उस टास्क क्यू में भेजती है. इससे एजेंट का काम शुरू हो जाता है.

import asyncio
import sys
import uuid

from temporalio.client import Client
from temporalio.contrib.pydantic import pydantic_data_converter


async def main():
    client = await Client.connect(
        "localhost:7233",
        data_converter=pydantic_data_converter,
    )

    query = sys.argv[1] if len(sys.argv) > 1 else "Tell me about recursion"

    result = await client.execute_workflow(
        "AgentWorkflow",
        query,
        id=f"gemini-agent-id-{uuid.uuid4()}",
        task_queue="gemini-agent-python-task-queue",
    )
    print(f"\nResult:\n{result}")


if __name__ == "__main__":
    asyncio.run(main())

एजेंट को चलाना

अगर आपने अब तक Temporal डेवलपमेंट सर्वर शुरू नहीं किया है, तो इसे शुरू करें:

temporal server start-dev

नई टर्मिनल विंडो में, एजेंट वर्कर शुरू करें:

python -m durable_agent_worker

तीसरी टर्मिनल विंडो में, अपने एजेंट को कोई क्वेरी सबमिट करें:

python -m start_workflow "are there any weather alerts for where I am?"

durable_agent_worker के टर्मिनल में मौजूद आउटपुट देखें. इससे आपको एजेंटिक लूप के हर इटरेशन में होने वाली कार्रवाइयों के बारे में पता चलेगा. एलएलएम, अपने पास मौजूद टूल का इस्तेमाल करके, उपयोगकर्ता के अनुरोध को पूरा कर सकता है. Temporal के यूज़र इंटरफ़ेस (यूआई) के ज़रिए किए गए चरणों को http://localhost:8233/namespaces/default/workflows पर देखा जा सकता है.

एजेंट के कॉल करने की वजह और कॉल टूल देखने के लिए, कुछ अलग-अलग प्रॉम्प्ट आज़माएं:

python -m start_workflow "are there any weather alerts for New York?"
python -m start_workflow "where am I?"
python -m start_workflow "what is my ip address?"
python -m start_workflow "tell me a joke"

आखिरी प्रॉम्प्ट के लिए किसी टूल की ज़रूरत नहीं है. इसलिए, एजेंट SYSTEM_INSTRUCTIONS के आधार पर हाइकू में जवाब देता है.

टेस्ट की अवधि (ज़रूरी नहीं)

टेम्पोरल का इस्तेमाल करने से, यह पक्का होता है कि आपका एजेंट बिना किसी रुकावट के काम करता रहे. इसकी जांच, दो अलग-अलग एक्सपेरिमेंट की मदद से की जा सकती है.

नेटवर्क बंद होने की स्थिति का सिम्युलेशन करना

इस टेस्ट में, आपको कुछ समय के लिए अपने कंप्यूटर का इंटरनेट कनेक्शन बंद करना होगा. इसके बाद, वर्कफ़्लो सबमिट करें. फिर, देखें कि Temporal अपने-आप फिर से कोशिश करता है या नहीं. इसके बाद, नेटवर्क को वापस चालू करें, ताकि यह देखा जा सके कि वह ठीक हो गया है या नहीं.

  1. अपनी मशीन को इंटरनेट से डिसकनेक्ट करें. उदाहरण के लिए, वाई-फ़ाई बंद करें.
  2. वर्कफ़्लो सबमिट करने के लिए:

    python -m start_workflow "tell me a joke"
  3. टेम्पोरल यूज़र इंटरफ़ेस (http://localhost:8233) देखें. आपको दिखेगा कि एलएलएम की गतिविधि पूरी नहीं हो पाई है और टेम्पोरल, बैकग्राउंड में फिर से कोशिश करने की प्रोसेस को अपने-आप मैनेज कर रहा है.

  4. इंटरनेट से फिर से कनेक्ट करें.

  5. अगली बार अपने-आप होने वाला अनुरोध, Gemini API तक पहुंच जाएगा. इसके बाद, आपके टर्मिनल पर फ़ाइनल नतीजा दिखेगा.

वर्कर के क्रैश होने पर सर्वाइव करना

इस टेस्ट में, आपको वर्कर को बीच में ही बंद करके फिर से चालू करना होता है. Temporal, वर्कफ़्लो के इतिहास (इवेंट सोर्सिंग) को फिर से चलाता है और पिछली पूरी की गई गतिविधि से शुरू होता है. पहले से पूरे किए गए एलएलएम इनवोकेशन और टूल कॉल को दोहराया नहीं जाता.

  1. वर्कर को बंद करने के लिए, durable_agent_worker.py खोलें और AgentWorkflow run लूप में मौजूद await asyncio.sleep(10) को कुछ समय के लिए अनकमेंट करें.
  2. वर्कर को रीस्टार्ट करें:

    python -m durable_agent_worker
  3. ऐसी क्वेरी सबमिट करें जिससे कई टूल ट्रिगर हों:

    python -m start_workflow "are there any weather alerts where I am?"
  4. प्रोसेस पूरी होने से पहले, वर्कर प्रोसेस को कभी भी बंद करें (वर्कर टर्मिनल में Ctrl-C या बैकग्राउंड में चल रही प्रोसेस के लिए kill %1 का इस्तेमाल करें).

  5. वर्कर को रीस्टार्ट करें:

    python -m durable_agent_worker

टेम्पोरल, वर्कफ़्लो के इतिहास को फिर से चलाता है. एलएलएम कॉल और टूल के इस्तेमाल से जुड़ी उन कार्रवाइयों को फिर से नहीं किया जाता जो पहले ही पूरी हो चुकी हैं. उनके नतीजे, इवेंट लॉग से तुरंत फिर से दिखाए जाते हैं. वर्कफ़्लो पूरा हो जाता है.

अतिरिक्त संसाधन