Skip to content

Commit 218e478

Browse files
Merge branch 'main' into copilot/change-default-api-endpoint
2 parents 0aad227 + 3f840b2 commit 218e478

File tree

10 files changed

+105
-17
lines changed

10 files changed

+105
-17
lines changed

.devcontainer/devcontainer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
},
1616
"ghcr.io/devcontainers/features/github-cli:1": {
1717
"version": "latest"
18+
},
19+
"ghcr.io/devcontainers/features/docker-in-docker:2": {
20+
"version": "latest"
1821
}
1922
},
2023
// Configure tool-specific properties

.github/workflows/smoketest.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ permissions:
1616
jobs:
1717
Linux:
1818
runs-on: ubuntu-latest
19+
environment: smoketest
1920
if: github.event.issue.pull_request # Make sure the comment is on a PR
2021
steps:
2122
- name: branch-deploy
@@ -24,6 +25,7 @@ jobs:
2425
with:
2526
trigger: "smoke test"
2627
reaction: "eyes"
28+
environment: "smoketest"
2729
stable_branch: "main"
2830
update_branch: "disabled"
2931

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,26 @@ taskflow:
457457

458458
The model version can then be updated by changing `gpt_latest` in the `model_config` file and applied across all taskflows that use the config.
459459

460+
In addition, model specific parameters can be provided via `model_config`. To do so, define a `model_settings` section in the `model_config` file. This section has to be a dictionary with the model names as keys:
461+
462+
```yaml
463+
model_settings:
464+
gpt_latest:
465+
temperature: 1
466+
reasoning:
467+
effort: high
468+
```
469+
470+
You do not need to set parameters for all models defined in the `models` section. When parameters are not set for a model, they'll fall back to the default value. However, all the settings in this section must belong to one of the models specified in the `models` section, otherwise an error will raise:
471+
472+
```yaml
473+
model_settings:
474+
new_model:
475+
...
476+
```
477+
478+
The above will result in an error because `new_model` is not defined in `models` section. Model parameters can also be set per task, and any settings defined in a task will override the settings in the config.
479+
460480
## Passing environment variables
461481

462482
Files of types `taskflow` and `toolbox` allow environment variables to be passed using the `env` field:

doc/GRAMMAR.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ Tasks can optionally specify which Model to use on the configured inference endp
9191

9292
Note that model identifiers may differ between OpenAI compatible endpoint providers, make sure you change your model identifier accordingly when switching providers. If not specified, a default LLM model (`gpt-4o`) is used.
9393

94+
Parameters to the model can also be specified in the task using the `model_settings` section:
95+
96+
```yaml
97+
model: gpt-5-mini
98+
model_settings:
99+
temperature: 1
100+
reasoning:
101+
effort: high
102+
```
103+
104+
If `model_settings` is absent, then the model parameters will fall back to either the default or the ones supplied in a `model_config`. However, any parameters supplied in the task will override those that are set in the `model_config`.
105+
94106
### Completion Requirement
95107

96108
Tasks can be marked as requiring completion, if a required task fails, the taskflow will abort. This defaults to false.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ dependencies = [
3939
"cyclopts==3.24.0",
4040
"distro==1.9.0",
4141
"dnspython==2.8.0",
42-
"docstring-to-markdown==0.17",
4342
"docstring_parser==0.17.0",
4443
"docutils==0.22",
4544
"email-validator==2.3.0",
@@ -73,6 +72,7 @@ dependencies = [
7372
"parse==1.20.2",
7473
"parso==0.8.4",
7574
"pathable==0.4.4",
75+
"platformdirs==4.5.0",
7676
"pluggy==1.6.0",
7777
"pycparser==2.23",
7878
"pydantic==2.11.7",

src/seclab_taskflow_agent/__main__.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async def deploy_task_agents(available_tools: AvailableTools,
107107
exclude_from_context: bool = False,
108108
max_turns: int = DEFAULT_MAX_TURNS,
109109
model: str = DEFAULT_MODEL,
110-
model_settings: ModelSettings | None = None,
110+
model_par: dict = {},
111111
run_hooks: TaskRunHooks | None = None,
112112
agent_hooks: TaskAgentHooks | None = None):
113113

@@ -130,10 +130,11 @@ async def deploy_task_agents(available_tools: AvailableTools,
130130

131131
# https://openai.github.io/openai-agents-python/ref/model_settings/
132132
parallel_tool_calls = True if os.getenv('MODEL_PARALLEL_TOOL_CALLS') else False
133-
model_settings = ModelSettings(
134-
temperature=os.getenv('MODEL_TEMP', default=0.0),
135-
tool_choice=('auto' if toolboxes else None),
136-
parallel_tool_calls=(parallel_tool_calls if toolboxes else None))
133+
model_params = {'temperature' : os.getenv('MODEL_TEMP', default = 0.0),
134+
'tool_choice' : ('auto' if toolboxes else None),
135+
'parallel_tool_calls' : (parallel_tool_calls if toolboxes else None)}
136+
model_params.update(model_par)
137+
model_settings = ModelSettings(**model_params)
137138

138139
# block tools if requested
139140
tool_filter = create_static_tool_filter(blocked_tool_names=blocked_tools) if blocked_tools else None
@@ -438,13 +439,22 @@ async def on_handoff_hook(
438439
global_variables.update(cli_globals)
439440
model_config = taskflow.get('model_config', {})
440441
model_keys = []
442+
models_params = {}
441443
if model_config:
442-
model_dict = available_tools.get_model_config(model_config)
443-
model_dict = model_dict.get('models', {})
444+
m_config = available_tools.get_model_config(model_config)
445+
model_dict = m_config.get('models', {})
444446
if model_dict:
445447
if not isinstance(model_dict, dict):
446448
raise ValueError(f"Models section of the model_config file {model_config} must be a dictionary")
447-
model_keys = model_dict.keys()
449+
model_keys = model_dict.keys()
450+
models_params = m_config.get('model_settings', {})
451+
if models_params and not isinstance(models_params, dict):
452+
raise ValueError(f"Settings section of model_config file {model_config} must be a dictionary")
453+
if not set(models_params.keys()).difference(model_keys).issubset(set([])):
454+
raise ValueError(f"Settings section of model_config file {model_config} contains models that are not in the model section")
455+
for k,v in models_params.items():
456+
if not isinstance(v, dict):
457+
raise ValueError(f"Settings for model {k} in model_config file {model_config} is not a dictionary")
448458

449459
for task in taskflow['taskflow']:
450460

@@ -465,8 +475,17 @@ async def on_handoff_hook(
465475
if k not in task_body:
466476
task_body[k] = v
467477
model = task_body.get('model', DEFAULT_MODEL)
478+
model_settings = {}
468479
if model in model_keys:
480+
if model in models_params:
481+
model_settings = models_params[model].copy()
469482
model = model_dict[model]
483+
task_model_settings = task_body.get('model_settings', {})
484+
if not isinstance(task_model_settings, dict):
485+
name = task.get('name', '')
486+
raise ValueError(f"model_settings in task {name} needs to be a dictionary")
487+
model_settings.update(task_model_settings)
488+
470489
# parse our taskflow grammar
471490
name = task_body.get('name', 'taskflow') # placeholder, not used yet
472491
description = task_body.get('description', 'taskflow') # placeholder not used yet
@@ -622,6 +641,7 @@ async def _deploy_task_agents(resolved_agents, prompt):
622641
on_tool_end=on_tool_end_hook,
623642
on_tool_start=on_tool_start_hook),
624643
model = model,
644+
model_par = model_settings,
625645
agent_hooks=TaskAgentHooks(
626646
on_handoff=on_handoff_hook))
627647
return result

src/seclab_taskflow_agent/mcp_servers/codeql/mcp_server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
import re
2121
from urllib.parse import urlparse, unquote
2222
import zipfile
23+
from seclab_taskflow_agent.path_utils import mcp_data_dir
2324

2425
mcp = FastMCP("CodeQL")
2526

26-
CODEQL_DBS_BASE_PATH = Path(os.getenv('CODEQL_DBS_BASE_PATH', default='/workspaces'))
27+
CODEQL_DBS_BASE_PATH = mcp_data_dir('seclab-taskflow-agent', 'codeql', 'CODEQL_DBS_BASE_PATH')
2728

2829
# tool name -> templated query lookup for supported languages
2930
TEMPLATED_QUERY_PATHS = {

src/seclab_taskflow_agent/mcp_servers/logbook/logbook.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
from fastmcp import FastMCP # move to FastMCP 2.0
1313
import json
1414
from pathlib import Path
15-
import os
15+
from seclab_taskflow_agent.path_utils import mcp_data_dir
1616

1717
mcp = FastMCP("Logbook")
1818

1919
LOG = {}
2020

21-
LOGBOOK = Path(__file__).parent.resolve() / Path(os.getenv('LOGBOOK_STATE_DIR', default='./')) / Path("logbook.json")
22-
21+
LOGBOOK = mcp_data_dir('seclab-taskflow-agent', 'logbook', 'LOGBOOK_STATE_DIR') / Path("logbook.json")
2322

2423
def ensure_log():
2524
global LOG

src/seclab_taskflow_agent/mcp_servers/memcache/memcache.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Any
1717
from .memcache_backend.dictionary_file import MemcacheDictionaryFileBackend
1818
from .memcache_backend.sqlite import SqliteBackend
19+
from seclab_taskflow_agent.path_utils import mcp_data_dir
1920

2021
mcp = FastMCP("Memcache")
2122

@@ -24,10 +25,7 @@
2425
'sqlite': SqliteBackend,
2526
}
2627

27-
# if MEMCACHE_STATE_DIR contains an absolute path we WANT the user to be able
28-
# to override the relative path in that case this path join will return
29-
# /MEMCACHE_STATE_DIR/memory.json
30-
MEMORY = Path(__file__).parent.resolve() / Path(os.getenv('MEMCACHE_STATE_DIR', default='./'))
28+
MEMORY = mcp_data_dir('seclab-taskflow-agent', 'memcache', 'MEMCACHE_STATE_DIR')
3129
BACKEND = os.getenv('MEMCACHE_BACKEND', default='sqlite')
3230

3331
backend = backends.get(BACKEND)(str(MEMORY))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SPDX-FileCopyrightText: 2025 GitHub
2+
# SPDX-License-Identifier: MIT
3+
4+
import platformdirs
5+
import os
6+
from pathlib import Path
7+
8+
9+
def mcp_data_dir(packagename: str, mcpname: str, env_override: str | None) -> Path:
10+
"""
11+
Create a directory for an MCP to store its data.
12+
13+
Parameters:
14+
packagename (str): The name of the package. Used as a subdirectory under the data directory.
15+
mcpname (str): The name of the MCP server. Used as a subdirectory under the package directory.
16+
env_override (str | None): The name of an environment variable that, if set, overrides the default data directory location. If None, the default location is used.
17+
18+
Returns:
19+
Path: The path to the created data directory for the MCP server.
20+
"""
21+
if env_override:
22+
p = os.getenv(env_override)
23+
if p:
24+
return Path(p)
25+
# Use [platformdirs](https://pypi.org/project/platformdirs/) to
26+
# choose an appropriate location.
27+
d = platformdirs.user_data_dir(appname="seclab-taskflow-agent",
28+
appauthor="GitHubSecurityLab",
29+
ensure_exists=True)
30+
# Each MCP server gets its own sub-directory
31+
p = Path(d).joinpath(packagename).joinpath(mcpname)
32+
p.mkdir(parents=True, exist_ok=True)
33+
return p

0 commit comments

Comments
 (0)