Skip to content

Commit 8b4e420

Browse files
Merge pull request #87 from GitHubSecurityLab/copilot/add-globals-command-line-support
Add command line support for global variables
2 parents 47445c6 + 03e98e5 commit 8b4e420

File tree

5 files changed

+129
-5
lines changed

5 files changed

+129
-5
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ Example: deploying a Taskflow:
7777
hatch run main -t examples.taskflows.example
7878
```
7979

80+
Example: deploying a Taskflow with command line global variables:
81+
82+
```sh
83+
hatch run main -t examples.taskflows.example_globals -g fruit=apples
84+
```
85+
86+
Multiple global variables can be set:
87+
88+
```sh
89+
hatch run main -t examples.taskflows.example_globals -g fruit=apples -g color=red
90+
```
91+
8092
## Deploying from Docker
8193

8294
You can deploy the Taskflow Agent via its Docker image using `docker/run.sh`.
@@ -104,6 +116,13 @@ Example: deploying a Taskflow (example.yaml):
104116
```sh
105117
docker/run.sh -t example
106118
```
119+
120+
Example: deploying a Taskflow with global variables:
121+
122+
```sh
123+
docker/run.sh -t example_globals -g fruit=apples
124+
```
125+
107126
Example: deploying a custom taskflow (custom_taskflow.yaml):
108127

109128
```sh

doc/GRAMMAR.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,20 @@ taskflow:
340340
Tell me more about {{ GLOBALS_fruit }}.
341341
```
342342
343+
Global variables can also be set or overridden from the command line using the `-g` or `--global` flag:
344+
345+
```sh
346+
hatch run main -t examples.taskflows.example_globals -g fruit=apples
347+
```
348+
349+
Multiple global variables can be set by repeating the flag:
350+
351+
```sh
352+
hatch run main -t examples.taskflows.example_globals -g fruit=apples -g color=red
353+
```
354+
355+
Command line globals override any globals defined in the taskflow YAML file, allowing you to reuse taskflows with different parameter values without editing the files.
356+
343357
### Reusable Tasks
344358

345359
Tasks can reuse single step taskflows and optionally override any of its configurations. This is done by setting a `uses` field with a link to the single step taskflow YAML file as its value.

src/seclab_taskflow_agent/__main__.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,35 @@ def parse_prompt_args(available_tools: AvailableTools,
6767
group.add_argument("-p", help="The personality to use (mutex with -t)", required=False)
6868
group.add_argument("-t", help="The taskflow to use (mutex with -p)", required=False)
6969
group.add_argument("-l", help="List available tool call models and exit", action='store_true', required=False)
70+
parser.add_argument("-g", "--global", dest="globals", action='append', help="Set global variable (KEY=VALUE). Can be used multiple times.", required=False)
7071
parser.add_argument('prompt', nargs=argparse.REMAINDER)
7172
#parser.add_argument('remainder', nargs=argparse.REMAINDER, help="Remaining args")
7273
help_msg = parser.format_help()
7374
help_msg += "\nExamples:\n\n"
7475
help_msg += "`-p assistant explain modems to me please`\n"
76+
help_msg += "`-t example -g fruit=apples`\n"
77+
help_msg += "`-t example -g fruit=apples -g color=red`\n"
7578
try:
7679
args = parser.parse_known_args(user_prompt.split(' ') if user_prompt else None)
7780
except SystemExit as e:
7881
if e.code == 2:
7982
logging.error(f"User provided incomplete prompt: {user_prompt}")
80-
return None, None, None, help_msg
83+
return None, None, None, None, help_msg
8184
p = args[0].p.strip() if args[0].p else None
8285
t = args[0].t.strip() if args[0].t else None
8386
l = args[0].l
84-
return p, t, l, ' '.join(args[0].prompt), help_msg
87+
88+
# Parse global variables from command line
89+
cli_globals = {}
90+
if args[0].globals:
91+
for g in args[0].globals:
92+
if '=' not in g:
93+
logging.error(f"Invalid global variable format: {g}. Expected KEY=VALUE")
94+
return None, None, None, None, None, help_msg
95+
key, value = g.split('=', 1)
96+
cli_globals[key.strip()] = value.strip()
97+
98+
return p, t, l, cli_globals, ' '.join(args[0].prompt), help_msg
8599

86100
async def deploy_task_agents(available_tools: AvailableTools,
87101
agents: dict,
@@ -378,7 +392,7 @@ async def _run_streamed():
378392

379393

380394
async def main(available_tools: AvailableTools,
381-
p: str | None, t: str | None, prompt: str | None):
395+
p: str | None, t: str | None, cli_globals: dict, prompt: str | None):
382396
last_mcp_tool_results = [] # XXX: memleaky
383397

384398
async def on_tool_end_hook(
@@ -418,7 +432,10 @@ async def on_handoff_hook(
418432
await render_model_output(f"** 🤖💪 Running Task Flow: {t}\n")
419433

420434
# optional global vars available for the taskflow tasks
435+
# Start with globals from taskflow file, then override with CLI globals
421436
global_variables = taskflow.get('globals', {})
437+
if cli_globals:
438+
global_variables.update(cli_globals)
422439
model_config = taskflow.get('model_config', {})
423440
model_keys = []
424441
if model_config:
@@ -646,7 +663,7 @@ async def _deploy_task_agents(resolved_agents, prompt):
646663
cwd = pathlib.Path.cwd()
647664
available_tools = AvailableTools()
648665

649-
p, t, l, user_prompt, help_msg = parse_prompt_args(available_tools)
666+
p, t, l, cli_globals, user_prompt, help_msg = parse_prompt_args(available_tools)
650667

651668
if l:
652669
tool_models = list_tool_call_models(os.getenv('COPILOT_TOKEN'))
@@ -658,4 +675,4 @@ async def _deploy_task_agents(resolved_agents, prompt):
658675
print(help_msg)
659676
sys.exit(1)
660677

661-
asyncio.run(main(available_tools, p, t, user_prompt), debug=True)
678+
asyncio.run(main(available_tools, p, t, cli_globals, user_prompt), debug=True)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-FileCopyrightText: 2025 GitHub
2+
# SPDX-License-Identifier: MIT
3+
4+
seclab-taskflow-agent:
5+
version: 1
6+
filetype: taskflow
7+
8+
globals:
9+
test_var: default_value
10+
taskflow:
11+
- task:
12+
run: |
13+
echo "test"

tests/test_yaml_parser.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,66 @@ def test_parse_example_taskflows(self):
4343
assert len(example_task_flow['taskflow']) == 4 # 4 tasks in taskflow
4444
assert example_task_flow['taskflow'][0]['task']['max_steps'] == 20
4545

46+
class TestCliGlobals:
47+
"""Test CLI global variable parsing."""
48+
49+
def test_parse_single_global(self):
50+
"""Test parsing a single global variable from command line."""
51+
from seclab_taskflow_agent.__main__ import parse_prompt_args
52+
available_tools = AvailableTools()
53+
54+
p, t, l, cli_globals, user_prompt, _ = parse_prompt_args(
55+
available_tools, "-t example -g fruit=apples")
56+
57+
assert t == "example"
58+
assert cli_globals == {"fruit": "apples"}
59+
assert p is None
60+
assert l is False
61+
62+
def test_parse_multiple_globals(self):
63+
"""Test parsing multiple global variables from command line."""
64+
from seclab_taskflow_agent.__main__ import parse_prompt_args
65+
available_tools = AvailableTools()
66+
67+
p, t, l, cli_globals, user_prompt, _ = parse_prompt_args(
68+
available_tools, "-t example -g fruit=apples -g color=red")
69+
70+
assert t == "example"
71+
assert cli_globals == {"fruit": "apples", "color": "red"}
72+
assert p is None
73+
assert l is False
74+
75+
def test_parse_global_with_spaces(self):
76+
"""Test parsing global variables with spaces in values."""
77+
from seclab_taskflow_agent.__main__ import parse_prompt_args
78+
available_tools = AvailableTools()
79+
80+
p, t, l, cli_globals, user_prompt, _ = parse_prompt_args(
81+
available_tools, "-t example -g message=hello world")
82+
83+
assert t == "example"
84+
# "world" becomes part of the prompt, not the value
85+
assert cli_globals == {"message": "hello"}
86+
assert "world" in user_prompt
87+
88+
def test_parse_global_with_equals_in_value(self):
89+
"""Test parsing global variables with equals sign in value."""
90+
from seclab_taskflow_agent.__main__ import parse_prompt_args
91+
available_tools = AvailableTools()
92+
93+
p, t, l, cli_globals, user_prompt, _ = parse_prompt_args(
94+
available_tools, "-t example -g equation=x=5")
95+
96+
assert t == "example"
97+
assert cli_globals == {"equation": "x=5"}
98+
99+
def test_globals_in_taskflow_file(self):
100+
"""Test that globals can be read from taskflow file."""
101+
available_tools = AvailableTools()
102+
103+
taskflow = available_tools.get_taskflow("tests.data.test_globals_taskflow")
104+
assert 'globals' in taskflow
105+
assert taskflow['globals']['test_var'] == 'default_value'
106+
46107
if __name__ == '__main__':
47108
pytest.main([__file__, '-v'])

0 commit comments

Comments
 (0)