Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record logs into step artifacts #1339

Merged
merged 46 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
79e5763
Record log entries into context
satansdeer Dec 6, 2024
c3cbf4b
Add log field to context
satansdeer Dec 6, 2024
f3debf5
Add SKYVERN_LOG artifact type
satansdeer Dec 6, 2024
c1e7ff6
Add skyvern log tab on the frontend
satansdeer Dec 6, 2024
b1f14c6
Add SkyvernLogEncoder to handle non-serializable objects
satansdeer Dec 6, 2024
b2cb9c4
Record logs to an artifact
satansdeer Dec 6, 2024
eb6fc3d
Record only logs corresponding to a step
satansdeer Dec 6, 2024
57cd772
Recover empty line
satansdeer Dec 6, 2024
419e12b
Add text log encoder
satansdeer Dec 9, 2024
ddbc340
re-enable upload block (#1324)
wintonzheng Dec 5, 2024
f313326
remove no latest screenshot error log (#1325)
LawyZheng Dec 5, 2024
9b35651
Put a guard in workflow save error detail (#1326)
wintonzheng Dec 5, 2024
2d90fed
urlencode download suffix (#1327)
LawyZheng Dec 5, 2024
be0e817
wait for downloads to be done (#1328)
LawyZheng Dec 5, 2024
5ee8c3b
Skyvern Forms UI (#1330)
wintonzheng Dec 5, 2024
f8a6b58
Fix a navigation bug with saved tasks (#1331)
wintonzheng Dec 5, 2024
8bf863c
workflow run block (#1332)
wintonzheng Dec 6, 2024
5d32d53
forloop metadata variables (#1334)
LawyZheng Dec 6, 2024
3c37a4a
auto prepend scheme to url (#1335)
LawyZheng Dec 6, 2024
1aa1146
rename GEMINI_FLUSH->GEMINI_FLASH (#1333)
nmfisher Dec 6, 2024
f3bb2fe
bump navigation max retry to 5 (#1336)
LawyZheng Dec 6, 2024
6606fba
add workflow_run_id column to artifacts + ObserverCruise and Observer…
wintonzheng Dec 6, 2024
aabc0fc
add observer cruise id to artifacts table (#1337)
wintonzheng Dec 6, 2024
b83c648
ObserverThought reproduce migration script (#1338)
wintonzheng Dec 6, 2024
40e9dd3
Import structlog
satansdeer Dec 11, 2024
5c8488d
Define build_log_uri method
satansdeer Dec 12, 2024
127b8a6
Merge main
satansdeer Dec 12, 2024
4742fc4
Extract save logs functions
satansdeer Dec 13, 2024
974b37e
Pass step_id to log artifact methods
satansdeer Dec 13, 2024
d48f792
Define get_artifact_by_entity_id
satansdeer Dec 13, 2024
f57c7b6
Reuse log artifacts when saving
satansdeer Dec 13, 2024
c8274c3
Introduce get artifacts by entity id handler
satansdeer Dec 13, 2024
3ff49cb
Get step artifacts using entity id handler
satansdeer Dec 13, 2024
c3ce7d3
Remove logs
satansdeer Dec 13, 2024
72b3f15
Record workflow run logs
satansdeer Dec 13, 2024
aac781f
Save task logs
satansdeer Dec 13, 2024
ff86b52
Merge branch 'main' into maksimi/record-logs
satansdeer Dec 13, 2024
a86d81d
Remove print
satansdeer Dec 13, 2024
0f0633f
Merge main
satansdeer Dec 16, 2024
727dd7e
Revert change to InvalidUrl type
satansdeer Dec 16, 2024
2312281
Add complete_action.action_order back
satansdeer Dec 16, 2024
d66e67b
Add with_skyvern_context decorator
satansdeer Dec 16, 2024
2cf5f69
Merge branch 'main' into shu/maksimi/record-logs-new
wintonzheng Dec 17, 2024
63829fa
address typing
wintonzheng Dec 17, 2024
9eda7ca
Add ENABLE_LOG_ARTIFACTS env variable; Disable log artifacts by default
satansdeer Dec 17, 2024
631f898
Pre-commit check
satansdeer Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions skyvern-frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const ArtifactType = {
LLMPrompt: "llm_prompt",
LLMRequest: "llm_request",
HTMLScrape: "html_scrape",
SkyvernLog: "skyvern_log",
} as const;

export type ArtifactType = (typeof ArtifactType)[keyof typeof ArtifactType];
Expand Down
8 changes: 8 additions & 0 deletions skyvern-frontend/src/routes/tasks/detail/StepArtifacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ function StepArtifacts({ id, stepProps }: Props) {
(artifact) => artifact.artifact_type === ArtifactType.HTMLScrape,
);

const skyvernLog = artifacts?.filter(
(artifact) => artifact.artifact_type === ArtifactType.SkyvernLog,
);

return (
<Tabs
value={artifact}
Expand Down Expand Up @@ -108,6 +112,7 @@ function StepArtifacts({ id, stepProps }: Props) {
<TabsTrigger value="llm_response_parsed">Action List</TabsTrigger>
<TabsTrigger value="html_raw">HTML (Raw)</TabsTrigger>
<TabsTrigger value="llm_request">LLM Request (Raw)</TabsTrigger>
<TabsTrigger value="skyvern_log">Skyvern Log</TabsTrigger>
</TabsList>
<TabsContent value="info">
<div className="flex flex-col gap-6 p-4">
Expand Down Expand Up @@ -209,6 +214,9 @@ function StepArtifacts({ id, stepProps }: Props) {
<TabsContent value="llm_request">
{llmRequest ? <Artifact type="json" artifacts={llmRequest} /> : null}
</TabsContent>
<TabsContent value="skyvern_log">
{skyvernLog ? <Artifact type="json" artifacts={skyvernLog} /> : null}
</TabsContent>
</Tabs>
);
}
Expand Down
18 changes: 18 additions & 0 deletions skyvern/forge/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from skyvern.webeye.browser_factory import BrowserState
from skyvern.webeye.scraper.scraper import ElementTreeFormat, ScrapedPage, scrape_website
from skyvern.webeye.utils.page import SkyvernFrame
from skyvern.forge.skyvern_log_encoder import SkyvernLogEncoder

LOG = structlog.get_logger()

Expand Down Expand Up @@ -1773,6 +1774,23 @@ async def update_step(
step_id=step.step_id,
diff=update_comparison,
)

try:
log = skyvern_context.current().log
log_json = json.dumps(log, cls=SkyvernLogEncoder, indent=2)
await app.ARTIFACT_MANAGER.create_artifact(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we can also capture task and workflow level logs? Even if we don't render them in the UI today

We historically associate logs like that w/ the first step, although @wintonzheng is currently overhauling it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/Skyvern-AI/skyvern/pull/1345/files

here's the example of supporting artifact upload for observer thought.

going forward we can add support for task level and workflow run level artifacts

step=step,
artifact_type=ArtifactType.SKYVERN_LOG,
data=log_json.encode(),
)
except Exception:
LOG.error(
"Failed to record skyvern log after action",
task_id=step.task_id,
step_id=step.step_id,
exc_info=True,
)

return await app.DATABASE.update_step(
task_id=step.task_id,
step_id=step.step_id,
Expand Down
2 changes: 2 additions & 0 deletions skyvern/forge/sdk/artifact/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class ArtifactType(StrEnum):
RECORDING = "recording"
BROWSER_CONSOLE_LOG = "browser_console_log"

SKYVERN_LOG = "skyvern_log"

# DEPRECATED. pls use SCREENSHOT_LLM, SCREENSHOT_ACTION or SCREENSHOT_FINAL
SCREENSHOT = "screenshot"

Expand Down
1 change: 1 addition & 0 deletions skyvern/forge/sdk/artifact/storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ArtifactType.SCREENSHOT_LLM: "png",
ArtifactType.SCREENSHOT_ACTION: "png",
ArtifactType.SCREENSHOT_FINAL: "png",
ArtifactType.SKYVERN_LOG: "json",
ArtifactType.LLM_PROMPT: "txt",
ArtifactType.LLM_REQUEST: "json",
ArtifactType.LLM_RESPONSE: "json",
Expand Down
1 change: 1 addition & 0 deletions skyvern/forge/sdk/core/skyvern_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SkyvernContext:
workflow_run_id: str | None = None
max_steps_override: int | None = None
totp_codes: dict[str, str | None] = field(default_factory=dict)
log: list[dict] = field(default_factory=list)

def __repr__(self) -> str:
return f"SkyvernContext(request_id={self.request_id}, organization_id={self.organization_id}, task_id={self.task_id}, workflow_id={self.workflow_id}, workflow_run_id={self.workflow_run_id}, max_steps_override={self.max_steps_override})"
Expand Down
18 changes: 16 additions & 2 deletions skyvern/forge/sdk/forge_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import structlog
from structlog.typing import EventDict

from skyvern.config import settings
from skyvern.forge.sdk.core import skyvern_context

Expand Down Expand Up @@ -54,6 +53,21 @@ def add_kv_pairs_to_msg(logger: logging.Logger, method_name: str, event_dict: Ev
return event_dict


def skyvern_logs_processor(logger: logging.Logger, method_name: str, event_dict: EventDict) -> EventDict:
"""
A custom processor to add skyvern logs to the context
"""
if method_name not in ["info", "warning", "error", "critical", "exception"]:
return event_dict

context = skyvern_context.current()
if context:
log_entry = dict(event_dict)
context.log.append(log_entry)

return event_dict


def setup_logger() -> None:
"""
Setup the logger with the specified format
Expand Down Expand Up @@ -88,7 +102,7 @@ def setup_logger() -> None:
structlog.processors.format_exc_info,
]
+ additional_processors
+ [renderer],
+ [skyvern_logs_processor, renderer],
)
uvicorn_error = logging.getLogger("uvicorn.error")
uvicorn_error.disabled = True
Expand Down
22 changes: 22 additions & 0 deletions skyvern/forge/skyvern_log_encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import json
from typing import Any

class SkyvernLogEncoder(json.JSONEncoder):
"""Custom JSON encoder for Skyvern logs that handles non-serializable objects"""
def default(self, obj: Any) -> Any:
if hasattr(obj, 'model_dump'):
return obj.model_dump()

if hasattr(obj, '__dataclass_fields__'):
return {k: getattr(obj, k) for k in obj.__dataclass_fields__}

if hasattr(obj, '__dict__'):
return {
'type': obj.__class__.__name__,
'attributes': {k: v for k, v in obj.__dict__.items() if not k.startswith('_')}
}
# Handle other non-serializable objects
try:
return str(obj)
except Exception:
return f"<non-serializable-{obj.__class__.__name__}>"