@@ -1234,3 +1234,242 @@ def _validate_response(response_text: str, schema: dict[str, Any]) -> dict[str,
12341234 raise ValueError (f"Response does not match schema: { e .message } " )
12351235
12361236 return response
1237+
1238+
1239+ # ===========================================================================
1240+ # Embedding Executor
1241+ # ===========================================================================
1242+
1243+
1244+ class EmbeddingInput (BlockInput ):
1245+ """Input schema for Embedding block.
1246+
1247+ Generates vector embeddings for text using OpenAI-compatible embedding API.
1248+ Works with OpenAI, LMStudio, Ollama (--api-compat), LocalAI, vLLM, and other
1249+ servers implementing the /v1/embeddings endpoint.
1250+
1251+ Defaults to 'embedding' profile from ~/.workflows/llm-config.yml.
1252+ """
1253+
1254+ profile : str = Field (
1255+ default = "embedding" ,
1256+ description = "Profile name from ~/.workflows/llm-config.yml (defaults to 'embedding')" ,
1257+ )
1258+
1259+ model : str | None = Field (
1260+ default = None ,
1261+ description = "Override embedding model (uses profile model if not specified)" ,
1262+ )
1263+
1264+ text : str = Field (
1265+ description = "Text to generate embedding for" ,
1266+ )
1267+
1268+ api_key : str | None = Field (
1269+ default = None ,
1270+ description = "Override API key (uses profile api_key_secret if not specified)" ,
1271+ )
1272+
1273+ api_url : str | None = Field (
1274+ default = None ,
1275+ description = "Override API endpoint URL (uses profile api_url if not specified)" ,
1276+ )
1277+
1278+ timeout : int | str = Field (
1279+ default = 30 ,
1280+ description = "Request timeout in seconds" ,
1281+ )
1282+
1283+ _validate_timeout = field_validator ("timeout" , mode = "before" )(
1284+ interpolatable_numeric_validator (int , ge = 1 , le = 300 )
1285+ )
1286+
1287+
1288+ class EmbeddingOutput (BlockOutput ):
1289+ """Output schema for Embedding block.
1290+
1291+ Returns the embedding vector and metadata.
1292+ """
1293+
1294+ embedding : list [float ] = Field (
1295+ default_factory = list ,
1296+ description = "Embedding vector (list of floats)" ,
1297+ )
1298+
1299+ dimensions : int = Field (
1300+ default = 0 ,
1301+ description = "Number of dimensions in the embedding" ,
1302+ )
1303+
1304+ success : bool = Field (
1305+ default = False ,
1306+ description = "Whether the embedding generation succeeded" ,
1307+ )
1308+
1309+ metadata : dict [str , Any ] = Field (
1310+ default_factory = dict ,
1311+ description = "Execution metadata (model, usage, etc.)" ,
1312+ )
1313+
1314+
1315+ class EmbeddingExecutor (BlockExecutor ):
1316+ """Executor for generating text embeddings using OpenAI-compatible API.
1317+
1318+ Works with any server implementing the OpenAI embeddings endpoint:
1319+ - OpenAI API (api.openai.com)
1320+ - LMStudio (localhost:1234)
1321+ - Ollama with OpenAI compatibility (localhost:11434/v1)
1322+ - LocalAI, vLLM, and other OpenAI-compatible servers
1323+
1324+ Configuration via ~/.workflows/llm-config.yml profile (defaults to 'embedding'):
1325+ ```yaml
1326+ profiles:
1327+ embedding:
1328+ provider: openai # or ollama, local, etc.
1329+ model: text-embedding-3-small
1330+ api_url: http://localhost:1234/v1 # for LMStudio
1331+ api_key_secret: OPENAI_API_KEY
1332+ ```
1333+
1334+ Example:
1335+ ```yaml
1336+ - id: embed_query
1337+ type: Embedding
1338+ inputs:
1339+ text: "{{inputs.query}}"
1340+ ```
1341+
1342+ Outputs:
1343+ - embedding: List of floats representing the embedding vector
1344+ - dimensions: Number of dimensions (e.g., 1536 for text-embedding-3-small)
1345+ - success: Whether the API call succeeded
1346+ - metadata: Contains model, usage, etc.
1347+ """
1348+
1349+ type_name : ClassVar [str ] = "Embedding"
1350+ input_type : ClassVar [type [BlockInput ]] = EmbeddingInput
1351+ output_type : ClassVar [type [BlockOutput ]] = EmbeddingOutput
1352+ examples : ClassVar [str ] = """```yaml
1353+ - id: embed_text
1354+ type: Embedding
1355+ inputs:
1356+ text: "Search for authentication bugs"
1357+ ```"""
1358+
1359+ security_level : ClassVar [ExecutorSecurityLevel ] = ExecutorSecurityLevel .TRUSTED
1360+ capabilities : ClassVar [ExecutorCapabilities ] = ExecutorCapabilities (can_network = True )
1361+
1362+ async def execute ( # type: ignore[override]
1363+ self , inputs : EmbeddingInput , context : Execution
1364+ ) -> EmbeddingOutput :
1365+ """Execute embedding generation using OpenAI-compatible API.
1366+
1367+ Resolves profile configuration and calls the embeddings endpoint.
1368+ Works with any OpenAI-compatible server (OpenAI, LMStudio, Ollama, etc.).
1369+
1370+ Raises:
1371+ ValueError: Invalid configuration
1372+ httpx.*: Network errors
1373+ """
1374+ execution_context = context .execution_context
1375+ if execution_context is None :
1376+ raise ValueError ("ExecutionContext not available" )
1377+
1378+ llm_config_loader = execution_context .llm_config_loader
1379+
1380+ # Start with input overrides
1381+ model = inputs .model
1382+ api_key = inputs .api_key
1383+ api_url = inputs .api_url
1384+
1385+ # Build inline overrides from inputs (filter out None values)
1386+ inline_overrides = {
1387+ key : value
1388+ for key , value in {
1389+ "model" : inputs .model ,
1390+ "api_url" : inputs .api_url ,
1391+ }.items ()
1392+ if value is not None
1393+ }
1394+
1395+ # Try to resolve profile (returns ResolvedLLMConfig with merged provider+profile values)
1396+ resolved_config = None
1397+ config = llm_config_loader .load_config ()
1398+
1399+ effective_profile : str | None = inputs .profile
1400+ if effective_profile not in config .profiles :
1401+ if config .default_profile and config .default_profile in config .profiles :
1402+ logger .warning (
1403+ f"Embedding profile '{ inputs .profile } ' not found, "
1404+ f"using default_profile '{ config .default_profile } '"
1405+ )
1406+ effective_profile = config .default_profile
1407+ else :
1408+ effective_profile = None
1409+
1410+ if effective_profile is not None :
1411+ resolved_config = llm_config_loader .resolve_profile (
1412+ profile = effective_profile , inline_overrides = inline_overrides
1413+ )
1414+
1415+ if resolved_config :
1416+ # Use resolved values as defaults (inputs override)
1417+ model = model or resolved_config .model
1418+ api_url = api_url or resolved_config .api_url
1419+
1420+ # Resolve API key from secrets if not provided
1421+ if api_key is None and resolved_config .api_key_secret :
1422+ from .secrets import EnvVarSecretProvider
1423+
1424+ secret_provider = EnvVarSecretProvider ()
1425+ api_key = await secret_provider .get_secret (resolved_config .api_key_secret )
1426+
1427+ # Default model if still not set
1428+ if model is None :
1429+ model = "text-embedding-3-small"
1430+
1431+ # Resolve timeout
1432+ timeout = resolve_interpolatable_numeric (inputs .timeout , int , "timeout" , ge = 1 , le = 300 )
1433+
1434+ try :
1435+ # Use OpenAI SDK which works with any OpenAI-compatible server
1436+ client = AsyncOpenAI (
1437+ api_key = api_key or "not-required" , # Some local servers don't need API key
1438+ base_url = api_url , # None = default OpenAI endpoint
1439+ timeout = float (timeout ),
1440+ )
1441+
1442+ response = await client .embeddings .create (
1443+ model = model ,
1444+ input = inputs .text ,
1445+ )
1446+
1447+ embedding = response .data [0 ].embedding
1448+ dimensions = len (embedding )
1449+
1450+ metadata : dict [str , Any ] = {
1451+ "model" : response .model ,
1452+ }
1453+
1454+ # Include usage if available (some servers may not return it)
1455+ if response .usage :
1456+ metadata ["usage" ] = {
1457+ "prompt_tokens" : response .usage .prompt_tokens ,
1458+ "total_tokens" : response .usage .total_tokens ,
1459+ }
1460+
1461+ return EmbeddingOutput (
1462+ embedding = embedding ,
1463+ dimensions = dimensions ,
1464+ success = True ,
1465+ metadata = metadata ,
1466+ )
1467+
1468+ except Exception as e :
1469+ logger .error (f"Embedding generation failed: { e } " )
1470+ return EmbeddingOutput (
1471+ embedding = [],
1472+ dimensions = 0 ,
1473+ success = False ,
1474+ metadata = {"error" : str (e )},
1475+ )
0 commit comments