Skip to content

Commit c861c20

Browse files
committed
feat(cortex): restructure agent phases and add atomic capabilities
Major refactoring of the Cortex agent architecture: - Add new atomic capabilities (backup-files, semantic-search, rrf-fusion, etc.) - Restructure phases from 5 to 9 for better separation of concerns - Add PID controller for adaptive execution - Remove deprecated cognitive capabilities - Update SQL model and executor improvements
1 parent c5f5c51 commit c861c20

48 files changed

Lines changed: 8686 additions & 2710 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,4 @@ src/workflows_mcp/templates/agents/*
230230
!src/workflows_mcp/templates/agents/iteration/
231231
!src/workflows_mcp/templates/agents/investigation/
232232
!src/workflows_mcp/templates/agents/cortex/
233+
!src/workflows_mcp/templates/agents/cortex-cell-v2/

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"openai>=1.68.0",
3131
"pathspec>=0.12.0",
3232
"tree-sitter-languages>=1.10.0",
33+
"sqlite-vec>=0.1.6",
3334
]
3435
[project.urls]
3536
Homepage = "https://github.com/qtsone/workflows-mcp"

src/workflows_mcp/engine/executor_base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def test_workflow():
635635
from .executors_http import HttpCallExecutor
636636
from .executors_image import ImageGenExecutor
637637
from .executors_interactive import PromptExecutor
638-
from .executors_llm import LLMCallExecutor
638+
from .executors_llm import EmbeddingExecutor, LLMCallExecutor
639639
from .executors_sql import SqlExecutor
640640
from .executors_state import (
641641
MergeJSONStateExecutor,
@@ -658,8 +658,9 @@ def test_workflow():
658658
# Register HTTP executor
659659
registry.register(HttpCallExecutor())
660660

661-
# Register LLM executor
661+
# Register LLM executors
662662
registry.register(LLMCallExecutor())
663+
registry.register(EmbeddingExecutor())
663664

664665
# Register Image executor
665666
registry.register(ImageGenExecutor())

src/workflows_mcp/engine/executors_llm.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
)

src/workflows_mcp/engine/sql/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
DatabaseEngine.MARIADB: "TINYINT(1)",
5555
},
5656
"json": {
57-
DatabaseEngine.SQLITE: "TEXT",
57+
DatabaseEngine.SQLITE: "JSON TEXT",
5858
DatabaseEngine.POSTGRESQL: "JSONB",
5959
DatabaseEngine.MARIADB: "JSON",
6060
},

src/workflows_mcp/engine/sql/sqlite_backend.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Foreign key enforcement enabled
1010
- Path validation and parent directory creation
1111
- PRAGMA configuration via options
12+
- sqlite-vec extension for vector similarity search
1213
"""
1314

1415
from __future__ import annotations
@@ -19,6 +20,8 @@
1920
from pathlib import Path
2021
from typing import Any
2122

23+
import sqlite_vec # type: ignore[import-untyped]
24+
2225
from .backend import ConnectionConfig, DatabaseBackendBase, DatabaseEngine, Params, QueryResult
2326

2427
logger = logging.getLogger(__name__)
@@ -89,6 +92,16 @@ def _connect() -> sqlite3.Connection:
8992
conn = sqlite3.connect(path, check_same_thread=False)
9093
conn.row_factory = sqlite3.Row
9194

95+
# Load sqlite-vec extension for vector similarity search
96+
# This must be done before any PRAGMA settings
97+
try:
98+
conn.enable_load_extension(True)
99+
sqlite_vec.load(conn)
100+
conn.enable_load_extension(False) # Disable for security
101+
logger.debug("Loaded sqlite-vec extension")
102+
except Exception as e:
103+
logger.warning(f"Failed to load sqlite-vec extension: {e}")
104+
92105
# Get PRAGMA settings from options or use defaults
93106
pragmas = {**self.DEFAULT_PRAGMAS}
94107
if config.options.get("sqlite_pragmas"):

0 commit comments

Comments
 (0)