66- Semantic Search: ~84% Hit@5
77- Improvement: 4x
88
9- Run with:
9+ Run with production API :
1010 STACKONE_API_KEY=xxx python tests/benchmark_search.py
11+
12+ Run with local Lambda (ai-generation/apps/action_search):
13+ # First, start the local Lambda:
14+ # cd ai-generation/apps/action_search && make run-local
15+ # Then run benchmark:
16+ python tests/benchmark_search.py --local
17+
18+ Environment Variables:
19+ STACKONE_API_KEY: Required for production mode
20+ LOCAL_LAMBDA_URL: Optional, defaults to http://localhost:4513/2015-03-31/functions/function/invocations
1121"""
1222
1323from __future__ import annotations
1424
25+ import argparse
1526import os
1627import time
1728from dataclasses import dataclass , field
18- from typing import Literal
29+ from typing import Any , Literal , Protocol
30+
31+ import httpx
1932
2033from stackone_ai import StackOneToolSet
21- from stackone_ai .semantic_search import SemanticSearchClient
34+ from stackone_ai .semantic_search import SemanticSearchClient , SemanticSearchResponse , SemanticSearchResult
2235from stackone_ai .utility_tools import ToolIndex
2336
37+ # Default local Lambda URL (from ai-generation/apps/action_search docker-compose)
38+ DEFAULT_LOCAL_LAMBDA_URL = "http://localhost:4513/2015-03-31/functions/function/invocations"
39+
40+
41+ class SearchClientProtocol (Protocol ):
42+ """Protocol for search clients (production or local)."""
43+
44+ def search (
45+ self ,
46+ query : str ,
47+ connector : str | None = None ,
48+ top_k : int = 10 ,
49+ ) -> SemanticSearchResponse : ...
50+
51+
52+ class LocalLambdaSearchClient :
53+ """Client for local action_search Lambda.
54+
55+ This client connects to the local Lambda running via docker-compose
56+ from ai-generation/apps/action_search.
57+
58+ Usage:
59+ # Start local Lambda first:
60+ # cd ai-generation/apps/action_search && make run-local
61+
62+ client = LocalLambdaSearchClient()
63+ response = client.search("create employee", connector="bamboohr", top_k=5)
64+ """
65+
66+ def __init__ (
67+ self ,
68+ lambda_url : str = DEFAULT_LOCAL_LAMBDA_URL ,
69+ timeout : float = 30.0 ,
70+ ) -> None :
71+ """Initialize the local Lambda client.
72+
73+ Args:
74+ lambda_url: URL of the local Lambda endpoint
75+ timeout: Request timeout in seconds
76+ """
77+ self .lambda_url = lambda_url
78+ self .timeout = timeout
79+
80+ def search (
81+ self ,
82+ query : str ,
83+ connector : str | None = None ,
84+ top_k : int = 10 ,
85+ ) -> SemanticSearchResponse :
86+ """Search for relevant actions using local Lambda.
87+
88+ Args:
89+ query: Natural language query
90+ connector: Optional connector filter
91+ top_k: Maximum number of results
92+
93+ Returns:
94+ SemanticSearchResponse with matching actions
95+ """
96+ # Lambda event envelope format
97+ payload : dict [str , Any ] = {
98+ "type" : "search" ,
99+ "payload" : {
100+ "query" : query ,
101+ "top_k" : top_k ,
102+ },
103+ }
104+ if connector :
105+ payload ["payload" ]["connector" ] = connector
106+
107+ try :
108+ response = httpx .post (
109+ self .lambda_url ,
110+ json = payload ,
111+ headers = {"Content-Type" : "application/json" },
112+ timeout = self .timeout ,
113+ )
114+ response .raise_for_status ()
115+ data = response .json ()
116+
117+ # Convert Lambda response to SemanticSearchResponse
118+ results = [
119+ SemanticSearchResult (
120+ action_name = r .get ("action_name" , "" ),
121+ connector_key = r .get ("connector_key" , "" ),
122+ similarity_score = r .get ("similarity_score" , 0.0 ),
123+ label = r .get ("label" , "" ),
124+ description = r .get ("description" , "" ),
125+ )
126+ for r in data .get ("results" , [])
127+ ]
128+ return SemanticSearchResponse (
129+ results = results ,
130+ total_count = data .get ("total_count" , len (results )),
131+ query = data .get ("query" , query ),
132+ )
133+ except httpx .RequestError as e :
134+ raise RuntimeError (f"Local Lambda request failed: { e } " ) from e
135+ except Exception as e :
136+ raise RuntimeError (f"Local Lambda search failed: { e } " ) from e
137+
24138
25139@dataclass
26140class EvaluationTask :
@@ -778,19 +892,17 @@ class SearchBenchmark:
778892 def __init__ (
779893 self ,
780894 tools : list ,
781- api_key : str ,
782- base_url : str = "https://api.stackone.com" ,
895+ semantic_client : SearchClientProtocol ,
783896 ):
784- """Initialize benchmark with tools and API credentials .
897+ """Initialize benchmark with tools and search client .
785898
786899 Args:
787900 tools: List of StackOneTool instances to search
788- api_key: StackOne API key for semantic search
789- base_url: Base URL for API requests
901+ semantic_client: Client for semantic search (production or local)
790902 """
791903 self .tools = tools
792904 self .local_index = ToolIndex (tools )
793- self .semantic_client = SemanticSearchClient ( api_key = api_key , base_url = base_url )
905+ self .semantic_client = semantic_client
794906
795907 def evaluate_local (
796908 self ,
@@ -942,22 +1054,38 @@ def print_report(report: ComparisonReport) -> None:
9421054 print (f" ... and { len (failed_local ) - 10 } more" )
9431055
9441056
945- def run_benchmark (api_key : str | None = None , base_url : str = "https://api.stackone.com" ) -> ComparisonReport :
1057+ def run_benchmark (
1058+ api_key : str | None = None ,
1059+ base_url : str = "https://api.stackone.com" ,
1060+ use_local : bool = False ,
1061+ local_lambda_url : str = DEFAULT_LOCAL_LAMBDA_URL ,
1062+ ) -> ComparisonReport :
9461063 """Run the full benchmark comparison.
9471064
9481065 Args:
9491066 api_key: StackOne API key (uses STACKONE_API_KEY env var if not provided)
950- base_url: Base URL for API requests
1067+ base_url: Base URL for production API requests
1068+ use_local: If True, use local Lambda instead of production API
1069+ local_lambda_url: URL of local Lambda endpoint
9511070
9521071 Returns:
9531072 ComparisonReport with results
9541073
9551074 Raises:
956- ValueError: If no API key is available
1075+ ValueError: If no API key is available (production mode only)
9571076 """
958- api_key = api_key or os .environ .get ("STACKONE_API_KEY" )
959- if not api_key :
960- raise ValueError ("API key must be provided or set via STACKONE_API_KEY environment variable" )
1077+ # Create semantic search client based on mode
1078+ if use_local :
1079+ print (f"Using LOCAL Lambda at: { local_lambda_url } " )
1080+ semantic_client : SearchClientProtocol = LocalLambdaSearchClient (lambda_url = local_lambda_url )
1081+ # For local mode, we still need API key for toolset but can use a dummy if not set
1082+ api_key = api_key or os .environ .get ("STACKONE_API_KEY" ) or "local-testing"
1083+ else :
1084+ api_key = api_key or os .environ .get ("STACKONE_API_KEY" )
1085+ if not api_key :
1086+ raise ValueError ("API key must be provided or set via STACKONE_API_KEY environment variable" )
1087+ print (f"Using PRODUCTION API at: { base_url } " )
1088+ semantic_client = SemanticSearchClient (api_key = api_key , base_url = base_url )
9611089
9621090 print ("Initializing toolset..." )
9631091 toolset = StackOneToolSet (api_key = api_key , base_url = base_url )
@@ -967,21 +1095,66 @@ def run_benchmark(api_key: str | None = None, base_url: str = "https://api.stack
9671095 print (f"Loaded { len (tools )} tools" )
9681096
9691097 print (f"\n Running benchmark with { len (EVALUATION_TASKS )} evaluation tasks..." )
970- benchmark = SearchBenchmark (list (tools ), api_key = api_key , base_url = base_url )
1098+ benchmark = SearchBenchmark (list (tools ), semantic_client = semantic_client )
9711099
9721100 report = benchmark .compare ()
9731101 print_report (report )
9741102
9751103 return report
9761104
9771105
978- if __name__ == "__main__" :
1106+ def main () -> None :
1107+ """Main entry point with CLI argument parsing."""
1108+ parser = argparse .ArgumentParser (
1109+ description = "Benchmark comparing local BM25+TF-IDF vs semantic search" ,
1110+ formatter_class = argparse .RawDescriptionHelpFormatter ,
1111+ epilog = """
1112+ Examples:
1113+ # Run with production API
1114+ STACKONE_API_KEY=xxx python tests/benchmark_search.py
1115+
1116+ # Run with local Lambda (start it first: cd ai-generation/apps/action_search && make run-local)
1117+ python tests/benchmark_search.py --local
1118+
1119+ # Run with custom local Lambda URL
1120+ python tests/benchmark_search.py --local --lambda-url http://localhost:9000/invoke
1121+ """ ,
1122+ )
1123+ parser .add_argument (
1124+ "--local" ,
1125+ action = "store_true" ,
1126+ help = "Use local Lambda instead of production API" ,
1127+ )
1128+ parser .add_argument (
1129+ "--lambda-url" ,
1130+ default = DEFAULT_LOCAL_LAMBDA_URL ,
1131+ help = f"Local Lambda URL (default: { DEFAULT_LOCAL_LAMBDA_URL } )" ,
1132+ )
1133+ parser .add_argument (
1134+ "--api-url" ,
1135+ default = "https://api.stackone.com" ,
1136+ help = "Production API base URL" ,
1137+ )
1138+
1139+ args = parser .parse_args ()
1140+
9791141 try :
980- run_benchmark ()
1142+ run_benchmark (
1143+ base_url = args .api_url ,
1144+ use_local = args .local ,
1145+ local_lambda_url = args .lambda_url ,
1146+ )
9811147 except ValueError as e :
9821148 print (f"Error: { e } " )
983- print ("Set STACKONE_API_KEY environment variable or pass api_key parameter " )
1149+ print ("Set STACKONE_API_KEY environment variable or use --local flag " )
9841150 exit (1 )
9851151 except Exception as e :
9861152 print (f"Benchmark failed: { e } " )
1153+ import traceback
1154+
1155+ traceback .print_exc ()
9871156 exit (1 )
1157+
1158+
1159+ if __name__ == "__main__" :
1160+ main ()
0 commit comments