teddy-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- teddy_cli-0.1.0.dist-info/LICENSE +677 -0
- teddy_cli-0.1.0.dist-info/METADATA +33 -0
- teddy_cli-0.1.0.dist-info/RECORD +143 -0
- teddy_cli-0.1.0.dist-info/WHEEL +4 -0
- teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
- teddy_executor/__init__.py +1 -0
- teddy_executor/__main__.py +335 -0
- teddy_executor/adapters/__init__.py +0 -0
- teddy_executor/adapters/inbound/__init__.py +0 -0
- teddy_executor/adapters/inbound/cli_formatter.py +107 -0
- teddy_executor/adapters/inbound/cli_helpers.py +249 -0
- teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
- teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
- teddy_executor/adapters/outbound/__init__.py +7 -0
- teddy_executor/adapters/outbound/console_interactor.py +212 -0
- teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
- teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
- teddy_executor/adapters/outbound/console_tooling.py +62 -0
- teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
- teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
- teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
- teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
- teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
- teddy_executor/adapters/outbound/shell_adapter.py +344 -0
- teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
- teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
- teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
- teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
- teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
- teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
- teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
- teddy_executor/container.py +333 -0
- teddy_executor/core/__init__.py +0 -0
- teddy_executor/core/domain/__init__.py +0 -0
- teddy_executor/core/domain/models/__init__.py +44 -0
- teddy_executor/core/domain/models/action_ports.py +28 -0
- teddy_executor/core/domain/models/change_set.py +10 -0
- teddy_executor/core/domain/models/exceptions.py +40 -0
- teddy_executor/core/domain/models/execution_report.py +65 -0
- teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
- teddy_executor/core/domain/models/plan.py +85 -0
- teddy_executor/core/domain/models/planning_ports.py +43 -0
- teddy_executor/core/domain/models/project_context.py +56 -0
- teddy_executor/core/domain/models/report_assembly_data.py +18 -0
- teddy_executor/core/domain/models/session.py +17 -0
- teddy_executor/core/domain/models/shell_output.py +12 -0
- teddy_executor/core/domain/models/web_search_results.py +26 -0
- teddy_executor/core/ports/__init__.py +0 -0
- teddy_executor/core/ports/inbound/__init__.py +0 -0
- teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
- teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
- teddy_executor/core/ports/inbound/init.py +15 -0
- teddy_executor/core/ports/inbound/plan_parser.py +52 -0
- teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
- teddy_executor/core/ports/inbound/plan_validator.py +26 -0
- teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
- teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
- teddy_executor/core/ports/outbound/__init__.py +34 -0
- teddy_executor/core/ports/outbound/config_service.py +29 -0
- teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
- teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
- teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
- teddy_executor/core/ports/outbound/llm_client.py +90 -0
- teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
- teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
- teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
- teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
- teddy_executor/core/ports/outbound/session_manager.py +97 -0
- teddy_executor/core/ports/outbound/session_repository.py +65 -0
- teddy_executor/core/ports/outbound/shell_executor.py +24 -0
- teddy_executor/core/ports/outbound/system_environment.py +25 -0
- teddy_executor/core/ports/outbound/time_service.py +28 -0
- teddy_executor/core/ports/outbound/user_interactor.py +126 -0
- teddy_executor/core/ports/outbound/web_scraper.py +24 -0
- teddy_executor/core/ports/outbound/web_searcher.py +25 -0
- teddy_executor/core/services/__init__.py +0 -0
- teddy_executor/core/services/action_changeset_builder.py +90 -0
- teddy_executor/core/services/action_diff_manager.py +110 -0
- teddy_executor/core/services/action_dispatcher.py +142 -0
- teddy_executor/core/services/action_executor.py +209 -0
- teddy_executor/core/services/action_factory.py +197 -0
- teddy_executor/core/services/action_parser_complex.py +216 -0
- teddy_executor/core/services/action_parser_strategies.py +84 -0
- teddy_executor/core/services/context_service.py +437 -0
- teddy_executor/core/services/edit_simulator.py +128 -0
- teddy_executor/core/services/execution_orchestrator.py +295 -0
- teddy_executor/core/services/execution_report_assembler.py +62 -0
- teddy_executor/core/services/init_service.py +80 -0
- teddy_executor/core/services/markdown_plan_parser.py +309 -0
- teddy_executor/core/services/markdown_report_formatter.py +143 -0
- teddy_executor/core/services/parser_infrastructure.py +222 -0
- teddy_executor/core/services/parser_metadata.py +153 -0
- teddy_executor/core/services/parser_reporting.py +267 -0
- teddy_executor/core/services/plan_validator.py +82 -0
- teddy_executor/core/services/planning_service.py +242 -0
- teddy_executor/core/services/prompt_manager.py +146 -0
- teddy_executor/core/services/session_lifecycle_manager.py +228 -0
- teddy_executor/core/services/session_loop_guard.py +46 -0
- teddy_executor/core/services/session_orchestrator.py +538 -0
- teddy_executor/core/services/session_planner.py +43 -0
- teddy_executor/core/services/session_pruning_service.py +438 -0
- teddy_executor/core/services/session_replanner.py +105 -0
- teddy_executor/core/services/session_repository.py +194 -0
- teddy_executor/core/services/session_service.py +529 -0
- teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
- teddy_executor/core/services/validation_rules/__init__.py +4 -0
- teddy_executor/core/services/validation_rules/edit.py +207 -0
- teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
- teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
- teddy_executor/core/services/validation_rules/execute.py +37 -0
- teddy_executor/core/services/validation_rules/filesystem.py +73 -0
- teddy_executor/core/services/validation_rules/helpers.py +178 -0
- teddy_executor/core/services/validation_rules/message.py +29 -0
- teddy_executor/core/utils/__init__.py +1 -0
- teddy_executor/core/utils/diff.py +57 -0
- teddy_executor/core/utils/io.py +75 -0
- teddy_executor/core/utils/markdown.py +131 -0
- teddy_executor/core/utils/serialization.py +39 -0
- teddy_executor/core/utils/string.py +351 -0
- teddy_executor/prompts.py +45 -0
- teddy_executor/registries/__init__.py +1 -0
- teddy_executor/registries/infrastructure.py +147 -0
- teddy_executor/registries/reviewer.py +57 -0
- teddy_executor/registries/validators.py +47 -0
- teddy_executor/resources/__init__.py +1 -0
- teddy_executor/resources/config/.gitignore +2 -0
- teddy_executor/resources/config/__init__.py +1 -0
- teddy_executor/resources/config/config.yaml +49 -0
- teddy_executor/resources/config/init.context +5 -0
- teddy_executor/resources/config/prompts/architect.xml +462 -0
- teddy_executor/resources/config/prompts/assistant.xml +336 -0
- teddy_executor/resources/config/prompts/debugger.xml +456 -0
- teddy_executor/resources/config/prompts/developer.xml +481 -0
- teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
- teddy_executor/resources/config/prompts/prototyper.xml +425 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
from teddy_executor.core.domain.models.action_ports import ActionPorts
|
|
3
|
+
from teddy_executor.core.domain.models.plan import DEFAULT_SIMILARITY_THRESHOLD
|
|
4
|
+
from teddy_executor.core.services.action_dispatcher import IAction, IActionFactory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ActionFactory(IActionFactory):
|
|
8
|
+
"""
|
|
9
|
+
A protocol-compliant factory that resolves action handlers from injected ports.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Maps uppercase verbs from Markdown plans to the internal, descriptive keys.
|
|
13
|
+
_MARKDOWN_ACTION_MAP = {
|
|
14
|
+
"CREATE": "create_file",
|
|
15
|
+
"EDIT": "edit",
|
|
16
|
+
"READ": "read",
|
|
17
|
+
"EXECUTE": "execute",
|
|
18
|
+
"MESSAGE": "message",
|
|
19
|
+
"RESEARCH": "research",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def __init__(self, ports: ActionPorts):
|
|
23
|
+
self._shell_executor = ports.shell_executor
|
|
24
|
+
self._file_system_manager = ports.file_system_manager
|
|
25
|
+
self._user_interactor = ports.user_interactor
|
|
26
|
+
self._web_scraper = ports.web_scraper
|
|
27
|
+
self._web_searcher = ports.web_searcher
|
|
28
|
+
self._config_service = ports.config_service
|
|
29
|
+
self._standalone_actions: set[str] = set()
|
|
30
|
+
self._action_map: Dict[str, Any] = {
|
|
31
|
+
"execute": self._shell_executor,
|
|
32
|
+
"create_file": self._file_system_manager,
|
|
33
|
+
"edit": self._file_system_manager,
|
|
34
|
+
"read_file": self._file_system_manager,
|
|
35
|
+
"message": self._user_interactor,
|
|
36
|
+
"research": self._web_searcher,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def _normalize_action_type(self, action_type: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Normalizes action types from different plan formats to the internal key format.
|
|
42
|
+
"""
|
|
43
|
+
# First, check the explicit mapping for Markdown verbs.
|
|
44
|
+
if action_type in self._MARKDOWN_ACTION_MAP:
|
|
45
|
+
return self._MARKDOWN_ACTION_MAP[action_type]
|
|
46
|
+
# Fallback to lowercasing for YAML/other formats.
|
|
47
|
+
return action_type.lower()
|
|
48
|
+
|
|
49
|
+
def _create_read_action(self, params: Optional[dict] = None) -> IAction:
|
|
50
|
+
"""Handles the special routing for the READ action."""
|
|
51
|
+
safe_params = params or {}
|
|
52
|
+
resource = safe_params.get("resource", safe_params.get("path", ""))
|
|
53
|
+
if resource.startswith("http"):
|
|
54
|
+
# Return a wrapper instead of monkeypatching the adapter
|
|
55
|
+
class WebReadAction:
|
|
56
|
+
def __init__(self, scraper):
|
|
57
|
+
self._scraper = scraper
|
|
58
|
+
|
|
59
|
+
def execute(self, **kwargs: Any) -> Any:
|
|
60
|
+
return self._scraper.get_content(url=kwargs["path"])
|
|
61
|
+
|
|
62
|
+
return WebReadAction(self._web_scraper)
|
|
63
|
+
|
|
64
|
+
if safe_params.get("lines"):
|
|
65
|
+
# Lines-aware read: use read_raw_file (bypass truncation) and extract range
|
|
66
|
+
from teddy_executor.core.utils.string import extract_lines_range # noqa: PLC0415
|
|
67
|
+
|
|
68
|
+
class LinesAwareReadAction:
|
|
69
|
+
def __init__(self, fs):
|
|
70
|
+
self._fs = fs
|
|
71
|
+
|
|
72
|
+
def execute(self, **kwargs: Any) -> Any:
|
|
73
|
+
path = kwargs.get("path", kwargs.get("resource", ""))
|
|
74
|
+
content = self._fs.read_raw_file(path=path)
|
|
75
|
+
lines_spec = kwargs.get("lines", "")
|
|
76
|
+
return extract_lines_range(content, lines_spec)
|
|
77
|
+
|
|
78
|
+
return LinesAwareReadAction(self._file_system_manager)
|
|
79
|
+
|
|
80
|
+
# Fall through to the standard file system handler for local files
|
|
81
|
+
return self._create_standard_action("read_file", params)
|
|
82
|
+
|
|
83
|
+
def _get_action_wrapper(self, handler: Any, method_name: str) -> IAction:
|
|
84
|
+
"""Returns an IAction wrapper around an adapter method."""
|
|
85
|
+
original_method = getattr(handler, method_name)
|
|
86
|
+
|
|
87
|
+
class ActionWrapper:
|
|
88
|
+
def __init__(self, factory, method):
|
|
89
|
+
self._factory = factory
|
|
90
|
+
self._method = method
|
|
91
|
+
|
|
92
|
+
def execute(self, **kwargs: Any) -> Any:
|
|
93
|
+
if "resource" in kwargs and "path" not in kwargs:
|
|
94
|
+
kwargs["path"] = kwargs.pop("resource")
|
|
95
|
+
|
|
96
|
+
if method_name == "execute":
|
|
97
|
+
return self._factory._handle_execute_protocol(self._method, kwargs)
|
|
98
|
+
if method_name == "edit_file":
|
|
99
|
+
return self._factory._handle_edit_protocol(self._method, kwargs)
|
|
100
|
+
if method_name == "ask_question":
|
|
101
|
+
return self._factory._handle_message_protocol(self._method, kwargs)
|
|
102
|
+
return self._method(**kwargs)
|
|
103
|
+
|
|
104
|
+
return ActionWrapper(self, original_method)
|
|
105
|
+
|
|
106
|
+
def _handle_execute_protocol(self, method: Any, kwargs: dict) -> Any:
|
|
107
|
+
"""Handles the complex parameter injection for the EXECUTE action."""
|
|
108
|
+
execute_params = {
|
|
109
|
+
k: v
|
|
110
|
+
for k, v in kwargs.items()
|
|
111
|
+
if k in ("command", "cwd", "env", "background", "timeout", "max_lines")
|
|
112
|
+
and v is not None
|
|
113
|
+
}
|
|
114
|
+
if "command" not in execute_params:
|
|
115
|
+
raise ValueError("'command' parameter is required for the execute action.")
|
|
116
|
+
|
|
117
|
+
# Extract Tail override from action params and convert to max_lines
|
|
118
|
+
tail = kwargs.get("tail")
|
|
119
|
+
if tail is not None:
|
|
120
|
+
try:
|
|
121
|
+
tail_int = int(tail)
|
|
122
|
+
if tail_int > 0:
|
|
123
|
+
execute_params["max_lines"] = tail_int
|
|
124
|
+
except (ValueError, TypeError):
|
|
125
|
+
pass # Invalid tail value, fall back to default
|
|
126
|
+
|
|
127
|
+
# Inject global timeout if not already specified in kwargs
|
|
128
|
+
if "timeout" not in execute_params and self._config_service:
|
|
129
|
+
# Safe-by-Default: Provide hardcoded 60.0 fallback if config is missing
|
|
130
|
+
default_timeout = self._config_service.get_setting(
|
|
131
|
+
"execution.default_timeout_seconds", 60.0
|
|
132
|
+
)
|
|
133
|
+
if default_timeout is not None:
|
|
134
|
+
execute_params["timeout"] = float(default_timeout)
|
|
135
|
+
|
|
136
|
+
return method(**execute_params)
|
|
137
|
+
|
|
138
|
+
def _handle_edit_protocol(self, method: Any, kwargs: dict) -> Any:
|
|
139
|
+
"""Handles the similarity threshold injection for the EDIT action."""
|
|
140
|
+
# 1. Inject from config if missing
|
|
141
|
+
if "similarity_threshold" not in kwargs and self._config_service:
|
|
142
|
+
# Safe-by-Default: Provide domain default if config is missing
|
|
143
|
+
global_threshold = self._config_service.get_setting(
|
|
144
|
+
"execution.similarity_threshold", DEFAULT_SIMILARITY_THRESHOLD
|
|
145
|
+
)
|
|
146
|
+
if global_threshold is not None:
|
|
147
|
+
kwargs["similarity_threshold"] = float(global_threshold)
|
|
148
|
+
return method(**kwargs)
|
|
149
|
+
|
|
150
|
+
def _handle_message_protocol(self, method: Any, kwargs: dict) -> Any:
|
|
151
|
+
"""Handles the positional argument mapping for the MESSAGE action."""
|
|
152
|
+
prompt = kwargs.get("prompt", kwargs.get("content", "")) or ""
|
|
153
|
+
return method(
|
|
154
|
+
prompt,
|
|
155
|
+
resources=kwargs.get("handoff_resources"),
|
|
156
|
+
agent_name=kwargs.get("agent_name"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _create_standard_action(
|
|
160
|
+
self, action_type: str, params: Optional[dict] = None
|
|
161
|
+
) -> IAction:
|
|
162
|
+
"""Creates an action handler for any action other than 'read'."""
|
|
163
|
+
action_type_key = self._normalize_action_type(action_type)
|
|
164
|
+
if action_type_key not in self._action_map:
|
|
165
|
+
raise ValueError(f"Unknown action type: '{action_type}'")
|
|
166
|
+
|
|
167
|
+
action_handler = self._action_map[action_type_key]
|
|
168
|
+
if action_handler in self._standalone_actions:
|
|
169
|
+
return action_handler()
|
|
170
|
+
|
|
171
|
+
method_map = {
|
|
172
|
+
"create_file": "create_file",
|
|
173
|
+
"edit": "edit_file",
|
|
174
|
+
"read_file": "read_file",
|
|
175
|
+
"message": "ask_question",
|
|
176
|
+
"research": "search",
|
|
177
|
+
"execute": "execute",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if action_type_key not in method_map:
|
|
181
|
+
if not hasattr(action_handler, "execute"):
|
|
182
|
+
raise NotImplementedError(
|
|
183
|
+
f"Adapter for {action_type} does not have a mapped method "
|
|
184
|
+
"or a default 'execute' method."
|
|
185
|
+
)
|
|
186
|
+
return action_handler
|
|
187
|
+
|
|
188
|
+
return self._get_action_wrapper(action_handler, method_map[action_type_key])
|
|
189
|
+
|
|
190
|
+
def create_action(self, action_type: str, params: Optional[dict] = None) -> IAction:
|
|
191
|
+
"""
|
|
192
|
+
Looks up the adapter protocol for the given action type and binds the correct
|
|
193
|
+
adapter method to the `execute` method required by the IAction protocol.
|
|
194
|
+
"""
|
|
195
|
+
if action_type.lower() == "read":
|
|
196
|
+
return self._create_read_action(params)
|
|
197
|
+
return self._create_standard_action(action_type, params)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
from teddy_executor.core.domain.models import ActionData, ActionType
|
|
8
|
+
from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
|
|
9
|
+
from teddy_executor.core.services.parser_infrastructure import (
|
|
10
|
+
_PeekableStream,
|
|
11
|
+
get_child_text,
|
|
12
|
+
get_action_heading,
|
|
13
|
+
consume_content_until_next_action,
|
|
14
|
+
)
|
|
15
|
+
from teddy_executor.core.services.parser_metadata import (
|
|
16
|
+
parse_action_metadata,
|
|
17
|
+
parse_env_from_metadata,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_find_replace_pair(stream: _PeekableStream) -> Optional[dict[str, Any]]:
|
|
22
|
+
from mistletoe.block_token import Heading, CodeFence, BlockCode
|
|
23
|
+
|
|
24
|
+
find_heading = stream.peek()
|
|
25
|
+
if not (
|
|
26
|
+
isinstance(find_heading, Heading) and "FIND:" in get_child_text(find_heading)
|
|
27
|
+
):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
stream.next()
|
|
31
|
+
find_code = stream.next()
|
|
32
|
+
if not isinstance(find_code, (CodeFence, BlockCode)):
|
|
33
|
+
raise InvalidPlanError(
|
|
34
|
+
"Missing code block for FIND in EDIT action.", offending_node=find_code
|
|
35
|
+
)
|
|
36
|
+
find_content = get_child_text(find_code).rstrip("\n")
|
|
37
|
+
|
|
38
|
+
replace_heading = stream.next()
|
|
39
|
+
if not (
|
|
40
|
+
isinstance(replace_heading, Heading)
|
|
41
|
+
and "REPLACE:" in get_child_text(replace_heading)
|
|
42
|
+
):
|
|
43
|
+
raise InvalidPlanError(
|
|
44
|
+
"Missing REPLACE block after FIND block",
|
|
45
|
+
offending_node=replace_heading,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
replace_code = stream.next()
|
|
49
|
+
if not isinstance(replace_code, (CodeFence, BlockCode)):
|
|
50
|
+
raise InvalidPlanError(
|
|
51
|
+
"Missing REPLACE block after FIND block",
|
|
52
|
+
offending_node=replace_code,
|
|
53
|
+
)
|
|
54
|
+
replace_content = get_child_text(replace_code).rstrip("\n")
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
"find": find_content,
|
|
58
|
+
"replace": replace_content,
|
|
59
|
+
"find_node": find_code,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_edit_action(
|
|
64
|
+
stream: _PeekableStream, valid_actions: set[str], node: Optional[Any] = None
|
|
65
|
+
) -> ActionData:
|
|
66
|
+
from mistletoe.block_token import List as MdList
|
|
67
|
+
|
|
68
|
+
metadata_list = stream.next()
|
|
69
|
+
if not isinstance(metadata_list, MdList):
|
|
70
|
+
raise InvalidPlanError(
|
|
71
|
+
"EDIT action is missing metadata list.", offending_node=metadata_list
|
|
72
|
+
)
|
|
73
|
+
description, params = parse_action_metadata(
|
|
74
|
+
metadata_list,
|
|
75
|
+
link_key_map={"File Path": "path"},
|
|
76
|
+
text_key_map={
|
|
77
|
+
"Match All": "match_all",
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if "match_all" in params:
|
|
82
|
+
params["match_all"] = str(params["match_all"]).lower() == "true"
|
|
83
|
+
|
|
84
|
+
edits = []
|
|
85
|
+
while stream.has_next():
|
|
86
|
+
if get_action_heading(stream.peek(), valid_actions):
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
pair = parse_find_replace_pair(stream)
|
|
90
|
+
if pair:
|
|
91
|
+
edits.append(pair)
|
|
92
|
+
else:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
if not edits:
|
|
96
|
+
raise InvalidPlanError(
|
|
97
|
+
"EDIT action found no valid FIND/REPLACE blocks.",
|
|
98
|
+
offending_node=node,
|
|
99
|
+
)
|
|
100
|
+
params["edits"] = edits
|
|
101
|
+
return ActionData(type="EDIT", description=description, params=params, node=node)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_execute_action(
|
|
105
|
+
stream: _PeekableStream, node: Optional[Any] = None
|
|
106
|
+
) -> ActionData:
|
|
107
|
+
from mistletoe.block_token import List as MdList, CodeFence
|
|
108
|
+
|
|
109
|
+
metadata_list = stream.next()
|
|
110
|
+
if not isinstance(metadata_list, MdList):
|
|
111
|
+
raise InvalidPlanError(
|
|
112
|
+
"EXECUTE action is missing metadata list.", offending_node=metadata_list
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
description, params = parse_action_metadata(
|
|
116
|
+
metadata_list,
|
|
117
|
+
text_key_map={
|
|
118
|
+
"Expected Outcome": "expected_outcome",
|
|
119
|
+
"cwd": "cwd",
|
|
120
|
+
"Allow Failure": "allow_failure",
|
|
121
|
+
"Background": "background",
|
|
122
|
+
"Timeout": "timeout",
|
|
123
|
+
"Tail": "tail",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if "allow_failure" in params:
|
|
128
|
+
params["allow_failure"] = params["allow_failure"].lower() == "true"
|
|
129
|
+
|
|
130
|
+
if "background" in params:
|
|
131
|
+
params["background"] = params["background"].lower() == "true"
|
|
132
|
+
|
|
133
|
+
if "timeout" in params and params["timeout"]:
|
|
134
|
+
try:
|
|
135
|
+
params["timeout"] = int(params["timeout"])
|
|
136
|
+
except ValueError:
|
|
137
|
+
# Leave as string, ActionFactory or validation will handle it
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
env_from_meta = parse_env_from_metadata(metadata_list)
|
|
141
|
+
if env_from_meta:
|
|
142
|
+
params["env"] = env_from_meta
|
|
143
|
+
|
|
144
|
+
command_block = stream.next()
|
|
145
|
+
if not isinstance(command_block, CodeFence):
|
|
146
|
+
raise InvalidPlanError(
|
|
147
|
+
"EXECUTE action is missing command code block.",
|
|
148
|
+
offending_node=command_block,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
params["command"] = get_child_text(command_block).strip()
|
|
152
|
+
|
|
153
|
+
return ActionData(type="EXECUTE", description=description, params=params, node=node)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_research_action(
|
|
157
|
+
stream: _PeekableStream, valid_actions: set[str], node: Optional[Any] = None
|
|
158
|
+
) -> ActionData:
|
|
159
|
+
from mistletoe.block_token import List as MdList, CodeFence
|
|
160
|
+
|
|
161
|
+
metadata_list = stream.next()
|
|
162
|
+
if not isinstance(metadata_list, MdList):
|
|
163
|
+
raise InvalidPlanError("RESEARCH action is missing metadata list.")
|
|
164
|
+
|
|
165
|
+
description, _ = parse_action_metadata(metadata_list)
|
|
166
|
+
content_nodes = consume_content_until_next_action(stream, valid_actions)
|
|
167
|
+
queries = []
|
|
168
|
+
for content_node in content_nodes:
|
|
169
|
+
if isinstance(content_node, CodeFence):
|
|
170
|
+
raw_content = get_child_text(content_node)
|
|
171
|
+
for line in raw_content.splitlines():
|
|
172
|
+
query = line.strip()
|
|
173
|
+
if query:
|
|
174
|
+
queries.append(query)
|
|
175
|
+
if not queries:
|
|
176
|
+
raise InvalidPlanError("RESEARCH action found no query code blocks.")
|
|
177
|
+
return ActionData(
|
|
178
|
+
type="RESEARCH",
|
|
179
|
+
description=description,
|
|
180
|
+
params={"queries": queries},
|
|
181
|
+
node=node,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_message_action(
|
|
186
|
+
stream: _PeekableStream,
|
|
187
|
+
node: Optional[Any] = None,
|
|
188
|
+
raw_content: Optional[str] = None,
|
|
189
|
+
) -> ActionData:
|
|
190
|
+
"""
|
|
191
|
+
Parses a MESSAGE action. If raw_content is provided (extracted directly
|
|
192
|
+
from the original plan text via heading line_number), use it as-is to
|
|
193
|
+
preserve blank lines and internal structure. Otherwise, fall back to
|
|
194
|
+
rendering AST nodes.
|
|
195
|
+
"""
|
|
196
|
+
content = ""
|
|
197
|
+
if raw_content is not None:
|
|
198
|
+
content = raw_content.rstrip("\n")
|
|
199
|
+
elif stream.has_next():
|
|
200
|
+
from mistletoe.markdown_renderer import MarkdownRenderer
|
|
201
|
+
|
|
202
|
+
nodes = []
|
|
203
|
+
while stream.has_next():
|
|
204
|
+
nodes.append(stream.next())
|
|
205
|
+
|
|
206
|
+
if nodes:
|
|
207
|
+
with MarkdownRenderer() as renderer:
|
|
208
|
+
rendered_parts = [renderer.render(node) for node in nodes]
|
|
209
|
+
content = "".join(rendered_parts).strip()
|
|
210
|
+
|
|
211
|
+
return ActionData(
|
|
212
|
+
type=ActionType.MESSAGE,
|
|
213
|
+
description="Message to user",
|
|
214
|
+
params={"content": content},
|
|
215
|
+
node=node,
|
|
216
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
from teddy_executor.core.domain.models import ActionData
|
|
8
|
+
from teddy_executor.core.ports.inbound.plan_parser import InvalidPlanError
|
|
9
|
+
from teddy_executor.core.services.parser_infrastructure import (
|
|
10
|
+
_PeekableStream,
|
|
11
|
+
)
|
|
12
|
+
from teddy_executor.core.services.parser_metadata import (
|
|
13
|
+
parse_action_metadata,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_create_action(
|
|
18
|
+
stream: _PeekableStream, node: Optional[Any] = None
|
|
19
|
+
) -> ActionData:
|
|
20
|
+
from mistletoe.block_token import List as MdList, CodeFence
|
|
21
|
+
|
|
22
|
+
metadata_list = stream.next()
|
|
23
|
+
if not isinstance(metadata_list, MdList):
|
|
24
|
+
raise InvalidPlanError(
|
|
25
|
+
"CREATE action is missing metadata list.", offending_node=metadata_list
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
description, params = parse_action_metadata(
|
|
29
|
+
metadata_list,
|
|
30
|
+
link_key_map={"File Path": "path"},
|
|
31
|
+
text_key_map={"Overwrite": "overwrite"},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if "overwrite" in params:
|
|
35
|
+
params["overwrite"] = params["overwrite"].lower() == "true"
|
|
36
|
+
|
|
37
|
+
code_block = stream.next()
|
|
38
|
+
if not isinstance(code_block, CodeFence):
|
|
39
|
+
raise InvalidPlanError(
|
|
40
|
+
"CREATE action is missing a content code block.", offending_node=code_block
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
params["content"] = ""
|
|
44
|
+
if code_block.children:
|
|
45
|
+
children = list(code_block.children)
|
|
46
|
+
if children:
|
|
47
|
+
child = children[0]
|
|
48
|
+
if hasattr(child, "content"):
|
|
49
|
+
params["content"] = child.content.rstrip("\n")
|
|
50
|
+
|
|
51
|
+
return ActionData(type="CREATE", description=description, params=params, node=node)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_resource_action(
|
|
55
|
+
stream: _PeekableStream, action_type: str, node: Optional[Any] = None
|
|
56
|
+
) -> ActionData:
|
|
57
|
+
from mistletoe.block_token import List as MdList
|
|
58
|
+
|
|
59
|
+
metadata_list = stream.next()
|
|
60
|
+
if not isinstance(metadata_list, MdList):
|
|
61
|
+
raise InvalidPlanError(
|
|
62
|
+
f"{action_type} action is missing metadata list.",
|
|
63
|
+
offending_node=metadata_list,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
description, params = parse_action_metadata(
|
|
67
|
+
metadata_list,
|
|
68
|
+
link_key_map={"Resource": "resource", "File Path": "path_alias"},
|
|
69
|
+
text_key_map={"Lines": "lines"},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if "path_alias" in params:
|
|
73
|
+
params["resource"] = params.pop("path_alias")
|
|
74
|
+
params["metadata_used_file_path_alias"] = True
|
|
75
|
+
|
|
76
|
+
return ActionData(
|
|
77
|
+
type=action_type, description=description, params=params, node=node
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_read_action(
|
|
82
|
+
stream: _PeekableStream, node: Optional[Any] = None
|
|
83
|
+
) -> ActionData:
|
|
84
|
+
return parse_resource_action(stream, "READ", node=node)
|