1313import logging
1414import os
1515import sys
16+ import uuid
1617from pathlib import Path
18+ from datetime import datetime
1719
1820from dotenv import load_dotenv
1921from rich .console import Console
@@ -68,6 +70,11 @@ def __init__(self):
6870 self .verifier = None
6971 self .secure_agent = None
7072 self .browser = None
73+ self .tracer = None
74+
75+ # Generate run ID for cloud tracing
76+ self .run_id = str (uuid .uuid4 ())
77+ self .run_label = f"predicate-secure-demo-{ datetime .now ().strftime ('%Y%m%d-%H%M%S' )} "
7178
7279 def _init_verifier (self ):
7380 """Initialize local LLM verifier."""
@@ -83,6 +90,39 @@ def _init_verifier(self):
8390 progress .update (task , completed = True )
8491 console .print ("[green]✓[/green] Verifier initialized\n " )
8592
93+ def _init_tracer (self ):
94+ """Initialize cloud tracer if API key is provided."""
95+ api_key = os .getenv ("PREDICATE_API_KEY" )
96+ if not api_key or self .tracer is not None :
97+ return
98+
99+ console .print ("\n [bold cyan]Initializing Cloud Tracer...[/bold cyan]" )
100+
101+ try :
102+ from predicate .tracer_factory import create_tracer
103+
104+ self .tracer = create_tracer (
105+ api_key = api_key ,
106+ run_id = self .run_id ,
107+ upload_trace = True ,
108+ goal = f"[demo] { self .task_description } " ,
109+ agent_type = "predicate-secure/demo" ,
110+ llm_model = "Qwen/Qwen2.5-7B-Instruct" ,
111+ start_url = self .start_url ,
112+ )
113+
114+ console .print ("[green]✓[/green] Cloud tracer initialized" )
115+ console .print (f" [dim]Run ID: { self .run_id } [/dim]" )
116+ console .print (f" [dim]Run Label: { self .run_label } [/dim]" )
117+ console .print (
118+ f" [dim]View trace in Predicate Studio: https://studio.predicatesystems.dev/runs/{ self .run_id } [/dim]\n "
119+ )
120+ except Exception as e :
121+ logger .warning (f"Failed to initialize cloud tracer: { e } " )
122+ console .print (
123+ f" [yellow]⚠[/yellow] Cloud tracer initialization failed: { e } \n "
124+ )
125+
86126 def _init_secure_agent (self ):
87127 """Initialize SecureAgent with predicate-authority integration."""
88128 if self .secure_agent is not None :
@@ -139,6 +179,7 @@ async def run_demo(self):
139179 try :
140180 # Step 1: Initialize components
141181 self ._init_verifier ()
182+ self ._init_tracer () # Initialize cloud tracer if API key provided
142183 self ._init_secure_agent ()
143184
144185 # Step 2: Initialize browser (with authorization)
@@ -205,13 +246,16 @@ async def _run_browser_task(self):
205246 executor = lambda : self .browser .goto (self .start_url ), # Returns coroutine
206247 )
207248
208- # Action 2: Take snapshot
209- await self ._authorized_action (
249+ # Action 2: Take snapshot to find clickable elements
250+ snapshot = await self ._authorized_action (
210251 action = "snapshot" ,
211252 target = "current_page" ,
212253 executor = lambda : self ._take_snapshot (), # Returns coroutine
213254 )
214255
256+ # Action 3: Find and click the "Learn more" link using semantic query
257+ await self ._find_and_click_link (snapshot , "Learn more" )
258+
215259 console .print ("\n [green]✓[/green] Task completed successfully\n " )
216260
217261 async def _authorized_action (self , action : str , target : str , executor : callable ):
@@ -231,6 +275,19 @@ async def _authorized_action(self, action: str, target: str, executor: callable)
231275 # For this demo, we'll simulate the authorization check
232276 authorized = self ._check_authorization (action , target )
233277
278+ # Emit authorization event to cloud tracer
279+ if self .tracer :
280+ self .tracer .emit (
281+ "authorization" ,
282+ data = {
283+ "action" : action ,
284+ "target" : target ,
285+ "principal" : self .principal_id ,
286+ "authorized" : authorized ,
287+ "policy_file" : str (self .policy_file ),
288+ },
289+ )
290+
234291 if not authorized :
235292 console .print (" [red]✗[/red] Action denied by policy" )
236293 raise PermissionError (f"Action { action } denied by authorization policy" )
@@ -279,12 +336,34 @@ async def _authorized_action(self, action: str, target: str, executor: callable)
279336 console .print (" [dim]Executing verifications...[/dim]" )
280337 all_passed = self ._execute_verifications (verification_plan )
281338
339+ # Emit verification event to cloud tracer
340+ if self .tracer :
341+ self .tracer .emit (
342+ "verification" ,
343+ data = {
344+ "action" : action ,
345+ "target" : target ,
346+ "verifications" : [
347+ {
348+ "predicate" : v .predicate ,
349+ "args" : v .args ,
350+ "passed" : v .passed if hasattr (v , "passed" ) else None ,
351+ }
352+ for v in verification_plan .verifications
353+ ],
354+ "reasoning" : verification_plan .reasoning ,
355+ "all_passed" : all_passed ,
356+ },
357+ )
358+
282359 if all_passed :
283360 console .print (" [green]✓[/green] All verifications passed" )
284361 else :
285362 console .print (" [red]✗[/red] Some verifications failed" )
286363 raise AssertionError ("Post-execution verification failed" )
287364
365+ return result
366+
288367 def _check_authorization (self , action : str , target : str ) -> bool :
289368 """Check if action is authorized by policy.
290369
@@ -315,6 +394,54 @@ def _check_authorization(self, action: str, target: str) -> bool:
315394 # For other actions, default to allow for demo
316395 return True
317396
397+ async def _find_and_click_link (self , snapshot , link_text : str ):
398+ """Find a link by text using semantic query and click it.
399+
400+ This demonstrates using the predicate SDK's find() function for
401+ semantic element selection from snapshot.
402+ """
403+ from predicate import find
404+
405+ console .print (f"\n [yellow]→[/yellow] Finding link with text: '{ link_text } '" )
406+
407+ # Use semantic query to find the link
408+ # The find() function returns the best match by importance
409+ element = find (snapshot , f"role=link text~'{ link_text } '" )
410+
411+ if not element :
412+ console .print (f" [yellow]⚠[/yellow] Link '{ link_text } ' not found, skipping click" )
413+ return
414+
415+ console .print (f" [green]✓[/green] Found element: { element .text } (ID: { element .id } )" )
416+ console .print (f" [dim]Role: { element .role } , Clickable: { element .visual_cues .is_clickable } [/dim]" )
417+
418+ # Click the element using the authorized action pattern
419+ # Post-verification will automatically check that URL contains "example-domains" after click
420+ await self ._authorized_action (
421+ action = "click" ,
422+ target = f"element#{ element .id } " ,
423+ executor = lambda : self ._click_element (element ), # Returns coroutine
424+ )
425+
426+ async def _click_element (self , element ):
427+ """Click an element by its ID."""
428+ # Use Playwright's selector to click the element
429+ # The element.id is the unique identifier from the snapshot
430+ selector = f"[data-sentience-id='{ element .id } ']"
431+
432+ try :
433+ await self .browser .page .click (selector , timeout = 5000 )
434+ console .print (f" [dim]Clicked element with selector: { selector } [/dim]" )
435+ except Exception as e :
436+ # Fallback: try clicking by XPath or other means
437+ console .print (f" [yellow]⚠[/yellow] Direct click failed, trying alternative: { e } " )
438+ # Use bounding box to click by coordinates
439+ await self .browser .page .mouse .click (
440+ element .bbox .x + element .bbox .width / 2 ,
441+ element .bbox .y + element .bbox .height / 2 ,
442+ )
443+ console .print (f" [dim]Clicked at coordinates: ({ element .bbox .x } , { element .bbox .y } )[/dim]" )
444+
318445 async def _get_page_summary (self ) -> str :
319446 """Get summary of current page state."""
320447 if not self .browser or not self .browser .page :
@@ -428,6 +555,18 @@ async def _cleanup(self):
428555 except Exception as e :
429556 logger .warning (f"Error closing browser: { e } " )
430557
558+ # Close cloud tracer (blocking to ensure upload completes)
559+ if self .tracer :
560+ try :
561+ console .print ("[dim]Uploading trace to Predicate Studio...[/dim]" )
562+ self .tracer .close (blocking = True )
563+ console .print ("[green]✓[/green] Trace uploaded" )
564+ console .print (
565+ f" [dim]View in Studio: https://studio.predicatesystems.dev/runs/{ self .run_id } [/dim]"
566+ )
567+ except Exception as e :
568+ logger .warning (f"Error closing tracer: { e } " )
569+
431570
432571async def main ():
433572 """Main entry point."""
0 commit comments