Record logs into step artifacts (#1339)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com> Co-authored-by: LawyZheng <lawyzheng1106@gmail.com> Co-authored-by: Nick Fisher <nick.fisher@avinium.com>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import asyncio
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Literal
|
||||
|
||||
import structlog
|
||||
|
||||
from skyvern.forge import app
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType, LogEntityType
|
||||
from skyvern.forge.sdk.db.id import generate_artifact_id
|
||||
from skyvern.forge.sdk.models import Step
|
||||
from skyvern.forge.sdk.schemas.observers import ObserverCruise, ObserverThought
|
||||
@@ -82,6 +81,35 @@ class ArtifactManager:
|
||||
path=path,
|
||||
)
|
||||
|
||||
async def create_log_artifact(
|
||||
self,
|
||||
log_entity_type: LogEntityType,
|
||||
log_entity_id: str,
|
||||
artifact_type: ArtifactType,
|
||||
step_id: str | None = None,
|
||||
task_id: str | None = None,
|
||||
workflow_run_id: str | None = None,
|
||||
workflow_run_block_id: str | None = None,
|
||||
organization_id: str | None = None,
|
||||
data: bytes | None = None,
|
||||
path: str | None = None,
|
||||
) -> str:
|
||||
artifact_id = generate_artifact_id()
|
||||
uri = app.STORAGE.build_log_uri(log_entity_type, log_entity_id, artifact_type)
|
||||
return await self._create_artifact(
|
||||
aio_task_primary_key=log_entity_id,
|
||||
artifact_id=artifact_id,
|
||||
artifact_type=artifact_type,
|
||||
uri=uri,
|
||||
step_id=step_id,
|
||||
task_id=task_id,
|
||||
workflow_run_id=workflow_run_id,
|
||||
workflow_run_block_id=workflow_run_block_id,
|
||||
organization_id=organization_id,
|
||||
data=data,
|
||||
path=path,
|
||||
)
|
||||
|
||||
async def create_observer_thought_artifact(
|
||||
self,
|
||||
observer_thought: ObserverThought,
|
||||
@@ -174,7 +202,7 @@ class ArtifactManager:
|
||||
artifact_id: str | None,
|
||||
organization_id: str | None,
|
||||
data: bytes,
|
||||
primary_key: Literal["task_id", "observer_thought_id", "observer_cruise_id"] = "task_id",
|
||||
primary_key: str = "task_id",
|
||||
) -> None:
|
||||
if not artifact_id or not organization_id:
|
||||
return None
|
||||
@@ -183,18 +211,10 @@ class ArtifactManager:
|
||||
return
|
||||
# Fire and forget
|
||||
aio_task = asyncio.create_task(app.STORAGE.store_artifact(artifact, data))
|
||||
if primary_key == "task_id":
|
||||
if not artifact.task_id:
|
||||
raise ValueError("Task ID is required to update artifact data.")
|
||||
self.upload_aiotasks_map[artifact.task_id].append(aio_task)
|
||||
elif primary_key == "observer_thought_id":
|
||||
if not artifact.observer_thought_id:
|
||||
raise ValueError("Observer Thought ID is required to update artifact data.")
|
||||
self.upload_aiotasks_map[artifact.observer_thought_id].append(aio_task)
|
||||
elif primary_key == "observer_cruise_id":
|
||||
if not artifact.observer_cruise_id:
|
||||
raise ValueError("Observer Cruise ID is required to update artifact data.")
|
||||
self.upload_aiotasks_map[artifact.observer_cruise_id].append(aio_task)
|
||||
|
||||
if not artifact[primary_key]:
|
||||
raise ValueError(f"{primary_key} is required to update artifact data.")
|
||||
self.upload_aiotasks_map[artifact[primary_key]].append(aio_task)
|
||||
|
||||
async def retrieve_artifact(self, artifact: Artifact) -> bytes | None:
|
||||
return await app.STORAGE.retrieve_artifact(artifact)
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_serializer
|
||||
|
||||
@@ -10,6 +11,9 @@ class ArtifactType(StrEnum):
|
||||
RECORDING = "recording"
|
||||
BROWSER_CONSOLE_LOG = "browser_console_log"
|
||||
|
||||
SKYVERN_LOG = "skyvern_log"
|
||||
SKYVERN_LOG_RAW = "skyvern_log_raw"
|
||||
|
||||
# DEPRECATED. pls use SCREENSHOT_LLM, SCREENSHOT_ACTION or SCREENSHOT_FINAL
|
||||
SCREENSHOT = "screenshot"
|
||||
|
||||
@@ -70,3 +74,13 @@ class Artifact(BaseModel):
|
||||
observer_thought_id: str | None = None
|
||||
signed_url: str | None = None
|
||||
organization_id: str | None = None
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return getattr(self, key)
|
||||
|
||||
|
||||
class LogEntityType(StrEnum):
|
||||
STEP = "step"
|
||||
TASK = "task"
|
||||
WORKFLOW_RUN = "workflow_run"
|
||||
WORKFLOW_RUN_BLOCK = "workflow_run_block"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType, LogEntityType
|
||||
from skyvern.forge.sdk.models import Step
|
||||
from skyvern.forge.sdk.schemas.observers import ObserverCruise, ObserverThought
|
||||
|
||||
@@ -11,6 +11,8 @@ FILE_EXTENTSION_MAP: dict[ArtifactType, str] = {
|
||||
ArtifactType.SCREENSHOT_LLM: "png",
|
||||
ArtifactType.SCREENSHOT_ACTION: "png",
|
||||
ArtifactType.SCREENSHOT_FINAL: "png",
|
||||
ArtifactType.SKYVERN_LOG: "log",
|
||||
ArtifactType.SKYVERN_LOG_RAW: "json",
|
||||
ArtifactType.LLM_PROMPT: "txt",
|
||||
ArtifactType.LLM_REQUEST: "json",
|
||||
ArtifactType.LLM_RESPONSE: "json",
|
||||
@@ -34,6 +36,10 @@ class BaseStorage(ABC):
|
||||
def build_uri(self, artifact_id: str, step: Step, artifact_type: ArtifactType) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def build_observer_thought_uri(
|
||||
self, artifact_id: str, observer_thought: ObserverThought, artifact_type: ArtifactType
|
||||
|
||||
@@ -8,7 +8,7 @@ import structlog
|
||||
|
||||
from skyvern.config import settings
|
||||
from skyvern.forge.sdk.api.files import get_download_dir, get_skyvern_temp_dir
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType, LogEntityType
|
||||
from skyvern.forge.sdk.artifact.storage.base import FILE_EXTENTSION_MAP, BaseStorage
|
||||
from skyvern.forge.sdk.models import Step
|
||||
from skyvern.forge.sdk.schemas.observers import ObserverCruise, ObserverThought
|
||||
@@ -24,6 +24,10 @@ class LocalStorage(BaseStorage):
|
||||
file_ext = FILE_EXTENTSION_MAP[artifact_type]
|
||||
return f"file://{self.artifact_path}/{step.task_id}/{step.order:02d}_{step.retry_index}_{step.step_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}"
|
||||
|
||||
def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str:
|
||||
file_ext = FILE_EXTENTSION_MAP[artifact_type]
|
||||
return f"file://{self.artifact_path}/logs/{log_entity_type}/{log_entity_id}/{datetime.utcnow().isoformat()}_{artifact_type}.{file_ext}"
|
||||
|
||||
def build_observer_thought_uri(
|
||||
self, artifact_id: str, observer_thought: ObserverThought, artifact_type: ArtifactType
|
||||
) -> str:
|
||||
|
||||
@@ -12,7 +12,7 @@ from skyvern.forge.sdk.api.files import (
|
||||
make_temp_directory,
|
||||
unzip_files,
|
||||
)
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType
|
||||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType, LogEntityType
|
||||
from skyvern.forge.sdk.artifact.storage.base import FILE_EXTENTSION_MAP, BaseStorage
|
||||
from skyvern.forge.sdk.models import Step
|
||||
from skyvern.forge.sdk.schemas.observers import ObserverCruise, ObserverThought
|
||||
@@ -27,6 +27,10 @@ class S3Storage(BaseStorage):
|
||||
file_ext = FILE_EXTENTSION_MAP[artifact_type]
|
||||
return f"s3://{self.bucket}/{settings.ENV}/{step.task_id}/{step.order:02d}_{step.retry_index}_{step.step_id}/{datetime.utcnow().isoformat()}_{artifact_id}_{artifact_type}.{file_ext}"
|
||||
|
||||
def build_log_uri(self, log_entity_type: LogEntityType, log_entity_id: str, artifact_type: ArtifactType) -> str:
|
||||
file_ext = FILE_EXTENTSION_MAP[artifact_type]
|
||||
return f"s3://{self.bucket}/{settings.ENV}/logs/{log_entity_type}/{log_entity_id}/{datetime.utcnow().isoformat()}_{artifact_type}.{file_ext}"
|
||||
|
||||
def build_observer_thought_uri(
|
||||
self, artifact_id: str, observer_thought: ObserverThought, artifact_type: ArtifactType
|
||||
) -> str:
|
||||
|
||||
Reference in New Issue
Block a user