ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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.
Potentially problematic release.
This version of ara-cli might be problematic. Click here for more details.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
ara_cli/prompt_handler.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
import litellm
|
|
3
|
-
from ara_cli.classifier import Classifier
|
|
4
|
-
from ara_cli.artefact_creator import ArtefactCreator
|
|
5
|
-
from ara_cli.template_manager import TemplatePathManager
|
|
6
|
-
from ara_cli.ara_config import ConfigManager
|
|
7
|
-
from ara_cli.file_lister import generate_markdown_listing
|
|
8
2
|
from os.path import exists, join
|
|
9
3
|
import os
|
|
10
4
|
from os import makedirs
|
|
@@ -12,94 +6,351 @@ from re import findall
|
|
|
12
6
|
import re
|
|
13
7
|
import shutil
|
|
14
8
|
import glob
|
|
9
|
+
import logging
|
|
10
|
+
import warnings
|
|
11
|
+
from io import StringIO
|
|
12
|
+
from contextlib import redirect_stderr
|
|
13
|
+
from langfuse import Langfuse
|
|
14
|
+
from langfuse.api.resources.commons.errors import Error as LangfuseError, NotFoundError
|
|
15
|
+
import litellm
|
|
16
|
+
from ara_cli.classifier import Classifier
|
|
17
|
+
from ara_cli.artefact_creator import ArtefactCreator
|
|
18
|
+
from ara_cli.template_manager import TemplatePathManager
|
|
19
|
+
from ara_cli.ara_config import ConfigManager
|
|
20
|
+
from ara_cli.file_lister import generate_markdown_listing
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
class LLMSingleton:
|
|
18
24
|
_instance = None
|
|
19
|
-
|
|
25
|
+
_default_model = None
|
|
26
|
+
_extraction_model = None
|
|
27
|
+
langfuse = None
|
|
20
28
|
|
|
21
|
-
def __init__(self,
|
|
29
|
+
def __init__(self, default_model_id, extraction_model_id):
|
|
22
30
|
config = ConfigManager().get_config()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
default_config_data = config.llm_config.get(str(default_model_id))
|
|
32
|
+
|
|
33
|
+
if not default_config_data:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"No configuration found for the default model: {default_model_id}"
|
|
36
|
+
)
|
|
37
|
+
self.default_config_params = default_config_data.model_dump(exclude_none=True)
|
|
38
|
+
|
|
39
|
+
extraction_config_data = config.llm_config.get(str(extraction_model_id))
|
|
40
|
+
if not extraction_config_data:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"No configuration found for the extraction model: {extraction_model_id}"
|
|
43
|
+
)
|
|
44
|
+
self.extraction_config_params = extraction_config_data.model_dump(
|
|
45
|
+
exclude_none=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
langfuse_public_key = os.getenv("ARA_CLI_LANGFUSE_PUBLIC_KEY")
|
|
49
|
+
langfuse_secret_key = os.getenv("ARA_CLI_LANGFUSE_SECRET_KEY")
|
|
50
|
+
langfuse_host = os.getenv("LANGFUSE_HOST")
|
|
51
|
+
|
|
52
|
+
captured_stderr = StringIO()
|
|
53
|
+
with redirect_stderr(captured_stderr):
|
|
54
|
+
self.langfuse = Langfuse(
|
|
55
|
+
public_key=langfuse_public_key,
|
|
56
|
+
secret_key=langfuse_secret_key,
|
|
57
|
+
host=langfuse_host,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Check if there was an authentication error
|
|
61
|
+
stderr_output = captured_stderr.getvalue()
|
|
62
|
+
if "Authentication error" in stderr_output:
|
|
63
|
+
warnings.warn(
|
|
64
|
+
"Invalid Langfuse credentials - prompt tracing disabled and using default prompts. "
|
|
65
|
+
"Set environment variables 'ARA_CLI_LANGFUSE_PUBLIC_KEY', 'ARA_CLI_LANGFUSE_SECRET_KEY', "
|
|
66
|
+
"'LANGFUSE_HOST' and restart application to use Langfuse capabilities",
|
|
67
|
+
UserWarning,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
LLMSingleton._default_model = default_model_id
|
|
71
|
+
LLMSingleton._extraction_model = extraction_model_id
|
|
33
72
|
LLMSingleton._instance = self
|
|
34
73
|
|
|
35
74
|
@classmethod
|
|
36
75
|
def get_instance(cls):
|
|
37
76
|
if cls._instance is None:
|
|
38
77
|
config = ConfigManager().get_config()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
default_model = config.default_llm
|
|
79
|
+
if not default_model:
|
|
80
|
+
if not config.llm_config:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"No LLM configurations are defined in the configuration file."
|
|
83
|
+
)
|
|
84
|
+
default_model = next(iter(config.llm_config))
|
|
85
|
+
|
|
86
|
+
extraction_model = getattr(config, "extraction_llm", default_model)
|
|
87
|
+
if not extraction_model:
|
|
88
|
+
extraction_model = default_model
|
|
89
|
+
|
|
90
|
+
cls(default_model, extraction_model)
|
|
91
|
+
return cls._instance
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def get_config_by_purpose(cls, purpose="default"):
|
|
95
|
+
"""
|
|
96
|
+
purpose= 'default' or 'extraction'
|
|
97
|
+
"""
|
|
98
|
+
instance = cls.get_instance()
|
|
99
|
+
if purpose == "extraction":
|
|
100
|
+
return instance.extraction_config_params.copy()
|
|
101
|
+
return instance.default_config_params.copy()
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def set_default_model(cls, model_name):
|
|
105
|
+
"""Sets the default language model for the current session."""
|
|
106
|
+
cls.get_instance()
|
|
107
|
+
if model_name == cls._default_model:
|
|
108
|
+
return cls._instance
|
|
109
|
+
cls(model_name, cls._extraction_model)
|
|
45
110
|
return cls._instance
|
|
46
111
|
|
|
47
112
|
@classmethod
|
|
48
|
-
def
|
|
49
|
-
|
|
113
|
+
def set_extraction_model(cls, model_name):
|
|
114
|
+
"""Sets the extraction language model for the current session."""
|
|
115
|
+
cls.get_instance()
|
|
116
|
+
if model_name == cls._extraction_model:
|
|
50
117
|
return cls._instance
|
|
51
|
-
cls(model_name)
|
|
52
|
-
print(f"Language model switched to '{model_name}'")
|
|
118
|
+
cls(cls._default_model, model_name)
|
|
53
119
|
return cls._instance
|
|
54
120
|
|
|
55
121
|
@classmethod
|
|
56
|
-
def
|
|
122
|
+
def get_default_model(cls):
|
|
123
|
+
"""Gets the default model name stored in the singleton instance."""
|
|
57
124
|
if cls._instance is None:
|
|
58
125
|
cls.get_instance()
|
|
59
|
-
return cls.
|
|
126
|
+
return cls._default_model
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def get_extraction_model(cls):
|
|
130
|
+
"""Gets the extraction model name stored in the singleton instance."""
|
|
131
|
+
if cls._instance is None:
|
|
132
|
+
cls.get_instance()
|
|
133
|
+
return cls._extraction_model
|
|
60
134
|
|
|
61
135
|
|
|
62
136
|
def write_string_to_file(filename, string, mode):
|
|
63
|
-
with open(filename, mode, encoding=
|
|
137
|
+
with open(filename, mode, encoding="utf-8") as file:
|
|
64
138
|
file.write(f"\n{string}\n")
|
|
65
139
|
return file
|
|
66
140
|
|
|
67
141
|
|
|
68
142
|
def read_string_from_file(path):
|
|
69
|
-
with open(path,
|
|
143
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
70
144
|
text = file.read()
|
|
71
145
|
return text
|
|
72
146
|
|
|
73
147
|
|
|
74
|
-
def
|
|
75
|
-
|
|
148
|
+
def _is_valid_message(message: dict) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Checks if a message in a prompt is valid (i.e., not empty).
|
|
151
|
+
It handles both string content and list content (for multimodal inputs).
|
|
152
|
+
"""
|
|
153
|
+
content = message.get("content")
|
|
154
|
+
|
|
155
|
+
if isinstance(content, str):
|
|
156
|
+
return content.strip() != ""
|
|
76
157
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
158
|
+
if isinstance(content, list):
|
|
159
|
+
# For multimodal content, check if there's at least one non-empty text part.
|
|
160
|
+
return any(
|
|
161
|
+
item.get("type") == "text" and item.get("text", "").strip() != ""
|
|
162
|
+
for item in content
|
|
163
|
+
)
|
|
80
164
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _norm(p: str) -> str:
|
|
169
|
+
"""Normalize slashes and collapse .. segments."""
|
|
170
|
+
return os.path.normpath(p) if p else p
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def resolve_existing_path(rel_or_abs_path: str, anchor_dir: str) -> str:
|
|
174
|
+
"""
|
|
175
|
+
Resolve a potentially relative path to an existing absolute path.
|
|
176
|
+
|
|
177
|
+
Strategy:
|
|
178
|
+
- If already absolute and exists -> return it.
|
|
179
|
+
- Else, try from the anchor_dir.
|
|
180
|
+
- Else, walk up parent directories from anchor_dir and try joining at each level.
|
|
181
|
+
- If nothing is found, return the normalized original (will fail later with clear message).
|
|
182
|
+
"""
|
|
183
|
+
if not rel_or_abs_path:
|
|
184
|
+
return rel_or_abs_path
|
|
185
|
+
|
|
186
|
+
candidate = _norm(rel_or_abs_path)
|
|
187
|
+
|
|
188
|
+
if os.path.isabs(candidate) and os.path.exists(candidate):
|
|
189
|
+
return candidate
|
|
190
|
+
|
|
191
|
+
anchor_dir = os.path.abspath(anchor_dir or os.getcwd())
|
|
192
|
+
|
|
193
|
+
# Try from anchor dir directly
|
|
194
|
+
direct = _norm(os.path.join(anchor_dir, candidate))
|
|
195
|
+
if os.path.exists(direct):
|
|
196
|
+
return direct
|
|
197
|
+
|
|
198
|
+
# Walk parents
|
|
199
|
+
cur = anchor_dir
|
|
200
|
+
prev = None
|
|
201
|
+
while cur and cur != prev:
|
|
202
|
+
test = _norm(os.path.join(cur, candidate))
|
|
203
|
+
if os.path.exists(test):
|
|
204
|
+
return test
|
|
205
|
+
prev = cur
|
|
206
|
+
cur = os.path.dirname(cur)
|
|
207
|
+
|
|
208
|
+
# Give back normalized candidate; open() will raise, but at least path is clean
|
|
209
|
+
return candidate
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def send_prompt(prompt, purpose="default"):
|
|
213
|
+
"""Prepares and sends a prompt to the LLM, streaming the response."""
|
|
214
|
+
chat_instance = LLMSingleton.get_instance()
|
|
215
|
+
config_parameters = chat_instance.get_config_by_purpose(purpose)
|
|
216
|
+
model_info = config_parameters.get("model", "unknown_model")
|
|
217
|
+
|
|
218
|
+
with LLMSingleton.get_instance().langfuse.start_as_current_span(
|
|
219
|
+
name="send_prompt"
|
|
220
|
+
) as span:
|
|
221
|
+
span.update_trace(
|
|
222
|
+
input={"prompt": prompt, "purpose": purpose, "model": model_info}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
config_parameters.pop("provider", None)
|
|
226
|
+
|
|
227
|
+
filtered_prompt = [msg for msg in prompt if _is_valid_message(msg)]
|
|
228
|
+
|
|
229
|
+
completion = litellm.completion(
|
|
230
|
+
**config_parameters, messages=filtered_prompt, stream=True
|
|
231
|
+
)
|
|
232
|
+
response_text = ""
|
|
233
|
+
try:
|
|
234
|
+
for chunk in completion:
|
|
235
|
+
chunk_content = chunk.choices[0].delta.content
|
|
236
|
+
if chunk_content:
|
|
237
|
+
response_text += chunk_content
|
|
238
|
+
yield chunk
|
|
239
|
+
|
|
240
|
+
# Update Langfuse span with success output
|
|
241
|
+
span.update(
|
|
242
|
+
output={
|
|
243
|
+
"success": True,
|
|
244
|
+
"response_length": len(response_text),
|
|
245
|
+
"response": response_text,
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
# Update Langfuse span with error details
|
|
251
|
+
span.update(output={"error": str(e)}, level="ERROR")
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def describe_image(image_path: str) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Send an image to the LLM and get a text description.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
image_path: Path to the image file
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Text description of the image
|
|
264
|
+
"""
|
|
265
|
+
with LLMSingleton.get_instance().langfuse.start_as_current_span(
|
|
266
|
+
name="ara-cli/describe-image"
|
|
267
|
+
) as span:
|
|
268
|
+
span.update_trace(input={"image_path": image_path})
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
langfuse_prompt = LLMSingleton.get_instance().langfuse.get_prompt(
|
|
272
|
+
"ara-cli/describe-image"
|
|
273
|
+
)
|
|
274
|
+
describe_image_prompt = (
|
|
275
|
+
langfuse_prompt.prompt if langfuse_prompt.prompt else None
|
|
276
|
+
)
|
|
277
|
+
except (LangfuseError, NotFoundError, Exception) as e:
|
|
278
|
+
logging.info(f"Could not fetch Langfuse prompt: {e}")
|
|
279
|
+
describe_image_prompt = None
|
|
280
|
+
|
|
281
|
+
# Fallback to default prompt if Langfuse prompt is not available
|
|
282
|
+
if not describe_image_prompt:
|
|
283
|
+
logging.info("Using default describe-image prompt.")
|
|
284
|
+
describe_image_prompt = (
|
|
285
|
+
"Please describe this image in detail. If it contains text, transcribe it exactly. "
|
|
286
|
+
"If it's a diagram or chart, explain its structure and content. If it's a photo or illustration, "
|
|
287
|
+
"describe what you see."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Resolve and read the image
|
|
291
|
+
resolved_image_path = resolve_existing_path(image_path, os.getcwd())
|
|
292
|
+
with open(resolved_image_path, "rb") as image_file:
|
|
293
|
+
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
|
|
294
|
+
|
|
295
|
+
# Determine image type
|
|
296
|
+
image_extension = os.path.splitext(resolved_image_path)[1].lower()
|
|
297
|
+
mime_type = {
|
|
298
|
+
".png": "image/png",
|
|
299
|
+
".jpg": "image/jpeg",
|
|
300
|
+
".jpeg": "image/jpeg",
|
|
301
|
+
".gif": "image/gif",
|
|
302
|
+
".bmp": "image/bmp",
|
|
303
|
+
}.get(image_extension, "image/png")
|
|
304
|
+
|
|
305
|
+
# Create message with image
|
|
306
|
+
message = {
|
|
307
|
+
"role": "user",
|
|
308
|
+
"content": [
|
|
309
|
+
{
|
|
310
|
+
"type": "text",
|
|
311
|
+
"text": describe_image_prompt,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"type": "image_url",
|
|
315
|
+
"image_url": {"url": f"data:{mime_type};base64,{base64_image}"},
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Get response from LLM using the extraction model purpose
|
|
321
|
+
response_text = ""
|
|
322
|
+
for chunk in send_prompt([message], purpose="extraction"):
|
|
323
|
+
chunk_content = chunk.choices[0].delta.content
|
|
324
|
+
if chunk_content:
|
|
325
|
+
response_text += chunk_content
|
|
326
|
+
|
|
327
|
+
response_text = response_text.strip()
|
|
328
|
+
|
|
329
|
+
span.update(
|
|
330
|
+
output={
|
|
331
|
+
"success": True,
|
|
332
|
+
"description_length": len(response_text),
|
|
333
|
+
"response": response_text,
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return response_text
|
|
89
338
|
|
|
90
339
|
|
|
91
340
|
def append_headings(classifier, param, heading_name):
|
|
92
341
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
93
342
|
|
|
94
|
-
artefact_data_path =
|
|
343
|
+
artefact_data_path = _norm(
|
|
344
|
+
f"ara/{sub_directory}/{param}.data/{classifier}.prompt_log.md"
|
|
345
|
+
)
|
|
95
346
|
|
|
96
347
|
# Check if the file exists, and if not, create an empty file
|
|
97
348
|
if not os.path.exists(artefact_data_path):
|
|
98
|
-
with open(artefact_data_path,
|
|
349
|
+
with open(artefact_data_path, "w", encoding="utf-8") as file:
|
|
99
350
|
file.write("")
|
|
100
351
|
|
|
101
352
|
content = read_string_from_file(artefact_data_path)
|
|
102
|
-
pattern = r
|
|
353
|
+
pattern = r"## {}_(\d+)".format(heading_name)
|
|
103
354
|
matches = findall(pattern, content)
|
|
104
355
|
|
|
105
356
|
max_number = 1
|
|
@@ -107,27 +358,27 @@ def append_headings(classifier, param, heading_name):
|
|
|
107
358
|
max_number = max(map(int, matches)) + 1
|
|
108
359
|
heading = f"## {heading_name}_{max_number}"
|
|
109
360
|
|
|
110
|
-
write_string_to_file(artefact_data_path, heading,
|
|
361
|
+
write_string_to_file(artefact_data_path, heading, "a")
|
|
111
362
|
|
|
112
363
|
|
|
113
364
|
def write_prompt_result(classifier, param, text):
|
|
114
365
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
write_string_to_file(artefact_data_path, text,
|
|
366
|
+
artefact_data_path = _norm(
|
|
367
|
+
f"ara/{sub_directory}/{param}.data/{classifier}.prompt_log.md"
|
|
368
|
+
)
|
|
369
|
+
write_string_to_file(artefact_data_path, text, "a")
|
|
119
370
|
|
|
120
371
|
|
|
121
372
|
def prompt_data_directory_creation(classifier, parameter):
|
|
122
373
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
123
|
-
prompt_data_path = f"ara/{sub_directory}/{parameter}.data/prompt.data"
|
|
374
|
+
prompt_data_path = _norm(f"ara/{sub_directory}/{parameter}.data/prompt.data")
|
|
124
375
|
if not exists(prompt_data_path):
|
|
125
376
|
makedirs(prompt_data_path)
|
|
126
377
|
return prompt_data_path
|
|
127
378
|
|
|
128
379
|
|
|
129
380
|
def get_file_content(path):
|
|
130
|
-
with open(path,
|
|
381
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
131
382
|
return file.read()
|
|
132
383
|
|
|
133
384
|
|
|
@@ -135,14 +386,25 @@ def initialize_prompt_templates(classifier, parameter):
|
|
|
135
386
|
prompt_data_path = prompt_data_directory_creation(classifier, parameter)
|
|
136
387
|
prompt_log_path = os.path.dirname(prompt_data_path)
|
|
137
388
|
|
|
138
|
-
template_path = os.path.join(os.path.dirname(__file__),
|
|
389
|
+
template_path = os.path.join(os.path.dirname(__file__), "templates")
|
|
139
390
|
artefact_creator = ArtefactCreator()
|
|
140
|
-
artefact_creator.create_artefact_prompt_files(
|
|
391
|
+
artefact_creator.create_artefact_prompt_files(
|
|
392
|
+
prompt_log_path, template_path, classifier
|
|
393
|
+
)
|
|
141
394
|
|
|
142
395
|
generate_config_prompt_template_file(prompt_data_path, "config.prompt_templates.md")
|
|
143
396
|
|
|
144
397
|
# Mark the relevant artefact in the givens list
|
|
145
|
-
generate_config_prompt_givens_file(
|
|
398
|
+
generate_config_prompt_givens_file(
|
|
399
|
+
prompt_data_path,
|
|
400
|
+
"config.prompt_givens.md",
|
|
401
|
+
artefact_to_mark=f"{parameter}.{classifier}",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Only once (was duplicated before)
|
|
405
|
+
generate_config_prompt_global_givens_file(
|
|
406
|
+
prompt_data_path, "config.prompt_global_givens.md"
|
|
407
|
+
)
|
|
146
408
|
|
|
147
409
|
|
|
148
410
|
def write_template_files_to_config(template_type, config_file, base_template_path):
|
|
@@ -154,14 +416,14 @@ def write_template_files_to_config(template_type, config_file, base_template_pat
|
|
|
154
416
|
|
|
155
417
|
def load_selected_prompt_templates(classifier, parameter):
|
|
156
418
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
157
|
-
prompt_data_path = f"ara/{sub_directory}/{parameter}.data/prompt.data"
|
|
419
|
+
prompt_data_path = _norm(f"ara/{sub_directory}/{parameter}.data/prompt.data")
|
|
158
420
|
config_file_path = os.path.join(prompt_data_path, "config.prompt_templates.md")
|
|
159
421
|
|
|
160
422
|
if not os.path.exists(config_file_path):
|
|
161
423
|
print("WARNING: config.prompt_templates.md does not exist.")
|
|
162
424
|
return
|
|
163
425
|
|
|
164
|
-
with open(config_file_path,
|
|
426
|
+
with open(config_file_path, "r", encoding="utf-8") as config_file:
|
|
165
427
|
content = config_file.read()
|
|
166
428
|
|
|
167
429
|
global_base_template_path = TemplatePathManager.get_template_base_path()
|
|
@@ -197,7 +459,9 @@ def find_files_with_endings(directory, endings):
|
|
|
197
459
|
# Create an empty dictionary to store files according to their endings
|
|
198
460
|
files_by_ending = {ending: [] for ending in endings}
|
|
199
461
|
|
|
200
|
-
files = [
|
|
462
|
+
files = [
|
|
463
|
+
f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))
|
|
464
|
+
]
|
|
201
465
|
# Walk through the files list
|
|
202
466
|
for file in files:
|
|
203
467
|
# Check each file to see if it ends with one of the specified endings
|
|
@@ -223,7 +487,7 @@ def move_and_copy_files(source_path, prompt_data_path, prompt_archive_path):
|
|
|
223
487
|
file_name = os.path.basename(source_path)
|
|
224
488
|
|
|
225
489
|
# Check the name ending and extension of source path
|
|
226
|
-
endings = [".commands.md", ".rules.md", ".intention.md"]
|
|
490
|
+
endings = [".blueprint.md", ".commands.md", ".rules.md", ".intention.md"]
|
|
227
491
|
if any(file_name.endswith(ext) for ext in endings):
|
|
228
492
|
for ext in endings:
|
|
229
493
|
if file_name.endswith(ext):
|
|
@@ -232,75 +496,114 @@ def move_and_copy_files(source_path, prompt_data_path, prompt_archive_path):
|
|
|
232
496
|
|
|
233
497
|
# Move all existing files with the same ending to the prompt_archive_path
|
|
234
498
|
for existing_file in glob.glob(glob_pattern):
|
|
235
|
-
archived_file_path = os.path.join(
|
|
499
|
+
archived_file_path = os.path.join(
|
|
500
|
+
prompt_archive_path, os.path.basename(existing_file)
|
|
501
|
+
)
|
|
236
502
|
shutil.move(existing_file, archived_file_path)
|
|
237
|
-
print(
|
|
238
|
-
|
|
503
|
+
print(
|
|
504
|
+
f"Moved existing prompt-module: {os.path.basename(existing_file)} to prompt.archive"
|
|
505
|
+
)
|
|
506
|
+
|
|
239
507
|
# Copy the source_path file to the prompt_data_path directory
|
|
240
508
|
target_path = os.path.join(prompt_data_path, file_name)
|
|
241
509
|
shutil.copy(source_path, target_path)
|
|
242
510
|
print(f"Loaded new prompt-module: {os.path.basename(target_path)}")
|
|
243
511
|
|
|
244
512
|
else:
|
|
245
|
-
print(
|
|
513
|
+
print(
|
|
514
|
+
f"File name {file_name} does not end with one of the specified patterns, skipping move and copy."
|
|
515
|
+
)
|
|
246
516
|
else:
|
|
247
517
|
print(f"WARNING: template {source_path} does not exist.")
|
|
248
518
|
|
|
249
519
|
|
|
250
520
|
def extract_and_load_markdown_files(md_prompt_file_path):
|
|
251
521
|
"""
|
|
252
|
-
Extracts markdown files paths based on checked items and constructs proper paths
|
|
522
|
+
Extracts markdown files paths based on checked items and constructs proper paths
|
|
523
|
+
respecting markdown header hierarchy. **Returns normalized relative paths**
|
|
524
|
+
(not resolved), and resolution happens later relative to the config file dir.
|
|
253
525
|
"""
|
|
254
526
|
header_stack = []
|
|
255
527
|
path_accumulator = []
|
|
256
|
-
with open(md_prompt_file_path,
|
|
528
|
+
with open(md_prompt_file_path, "r", encoding="utf-8") as file:
|
|
257
529
|
for line in file:
|
|
258
|
-
if line.strip().startswith(
|
|
259
|
-
level = line.count(
|
|
260
|
-
header = line.strip().strip(
|
|
530
|
+
if line.strip().startswith("#"):
|
|
531
|
+
level = line.count("#")
|
|
532
|
+
header = line.strip().strip("#").strip()
|
|
261
533
|
# Adjust the stack based on the current header level
|
|
262
534
|
current_depth = len(header_stack)
|
|
263
535
|
if level <= current_depth:
|
|
264
|
-
header_stack = header_stack[:level-1]
|
|
536
|
+
header_stack = header_stack[: level - 1]
|
|
265
537
|
header_stack.append(header)
|
|
266
|
-
elif
|
|
267
|
-
relative_path = line.split(
|
|
268
|
-
|
|
269
|
-
|
|
538
|
+
elif "[x]" in line:
|
|
539
|
+
relative_path = line.split("]")[-1].strip()
|
|
540
|
+
# Use os.path.join for OS-safe joining, then normalize
|
|
541
|
+
full_rel_path = os.path.join(*header_stack, relative_path) if header_stack else relative_path
|
|
542
|
+
path_accumulator.append(_norm(full_rel_path))
|
|
270
543
|
return path_accumulator
|
|
271
544
|
|
|
272
545
|
|
|
273
546
|
def load_givens(file_path):
|
|
274
|
-
|
|
547
|
+
"""
|
|
548
|
+
Reads marked givens from a config markdown and returns:
|
|
549
|
+
- combined markdown content (including code fences / images)
|
|
550
|
+
- a list of image data dicts for the multimodal message
|
|
551
|
+
Paths inside the markdown are resolved robustly relative to the config file directory (and its parents).
|
|
552
|
+
"""
|
|
553
|
+
content = ""
|
|
275
554
|
image_data_list = []
|
|
276
555
|
markdown_items = extract_and_load_markdown_files(file_path)
|
|
277
556
|
|
|
557
|
+
if not markdown_items:
|
|
558
|
+
return "", []
|
|
559
|
+
|
|
560
|
+
content = "### GIVENS\n\n"
|
|
561
|
+
|
|
562
|
+
anchor_dir = os.path.dirname(os.path.abspath(file_path))
|
|
563
|
+
|
|
278
564
|
for item in markdown_items:
|
|
279
|
-
|
|
280
|
-
|
|
565
|
+
resolved = resolve_existing_path(item, anchor_dir)
|
|
566
|
+
# Keep the listing line readable, show the original relative item
|
|
567
|
+
content += item + "\n"
|
|
568
|
+
|
|
569
|
+
ext = os.path.splitext(resolved)[1].lower()
|
|
570
|
+
|
|
571
|
+
# Image branch
|
|
572
|
+
if ext in (".png", ".jpeg", ".jpg", ".gif", ".bmp"):
|
|
573
|
+
with open(resolved, "rb") as image_file:
|
|
281
574
|
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
|
|
282
|
-
image_data_list.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_image}"}})
|
|
283
|
-
content += item + "\n"
|
|
284
|
-
content += f'' + "\n"
|
|
285
|
-
else:
|
|
286
|
-
# Check if the item specifies line ranges
|
|
287
|
-
# TODO item has currently no trailing [] see extraction and handover method in extract and load
|
|
288
|
-
# item = f"[10:29] {item}"
|
|
289
|
-
# print(f"found {item}, check for subsection")
|
|
290
|
-
# TODO re.match can not split the item with [] correctly and extract the line numbers
|
|
291
|
-
# TODO logic of subsections is not supported by the update algorithm of the config prompt givens updater
|
|
292
|
-
# TODO extract in lines of *.md files potential images and add them to the image list
|
|
293
575
|
|
|
576
|
+
mime_type = {
|
|
577
|
+
".png": "image/png",
|
|
578
|
+
".jpg": "image/jpeg",
|
|
579
|
+
".jpeg": "image/jpeg",
|
|
580
|
+
".gif": "image/gif",
|
|
581
|
+
".bmp": "image/bmp",
|
|
582
|
+
}.get(ext, "image/png")
|
|
583
|
+
|
|
584
|
+
image_data_list.append(
|
|
585
|
+
{
|
|
586
|
+
"type": "image_url",
|
|
587
|
+
"image_url": {"url": f"data:{mime_type};base64,{base64_image}"},
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
# Also embed inline for the prompt markdown (use png as a neutral default for data URI)
|
|
591
|
+
content += f"\n"
|
|
592
|
+
|
|
593
|
+
else:
|
|
594
|
+
# Check if the item specifies line ranges: e.g. "[10:20,25:30] filePath"
|
|
294
595
|
match = re.match(r".*?\[(\d+:\d+(?:,\s*\d+:\d+)*)\]\s+(.+)", item)
|
|
295
596
|
if match:
|
|
296
597
|
line_ranges, file_name = match.groups()
|
|
297
|
-
|
|
298
|
-
content +=
|
|
598
|
+
resolved_sub = resolve_existing_path(file_name, anchor_dir)
|
|
599
|
+
content += "```\n"
|
|
600
|
+
content += get_partial_file_content(resolved_sub, line_ranges) + "\n"
|
|
299
601
|
content += "```\n\n"
|
|
300
602
|
else:
|
|
301
|
-
content +=
|
|
302
|
-
content += get_file_content(
|
|
603
|
+
content += "```\n"
|
|
604
|
+
content += get_file_content(resolved) + "\n"
|
|
303
605
|
content += "```\n\n"
|
|
606
|
+
|
|
304
607
|
return content, image_data_list
|
|
305
608
|
|
|
306
609
|
|
|
@@ -309,25 +612,25 @@ def get_partial_file_content(file_name, line_ranges):
|
|
|
309
612
|
Reads specific lines from a file based on the line ranges provided.
|
|
310
613
|
|
|
311
614
|
Args:
|
|
312
|
-
file_name (str): The path to the file.
|
|
615
|
+
file_name (str): The path to the file (absolute or relative, already resolved by caller).
|
|
313
616
|
line_ranges (str): A string representing the line ranges to read, e.g., '10:20,25:30'.
|
|
314
617
|
|
|
315
618
|
Returns:
|
|
316
619
|
str: The content of the specified lines.
|
|
317
620
|
"""
|
|
318
|
-
line_ranges = line_ranges.strip(
|
|
621
|
+
line_ranges = line_ranges.strip("[]").split(",")
|
|
319
622
|
lines_to_read = []
|
|
320
623
|
for line_range in line_ranges:
|
|
321
|
-
start, end = map(int, line_range.split(
|
|
624
|
+
start, end = map(int, line_range.split(":"))
|
|
322
625
|
lines_to_read.extend(range(start, end + 1))
|
|
323
626
|
|
|
324
627
|
partial_content = []
|
|
325
|
-
with open(file_name,
|
|
628
|
+
with open(file_name, "r", encoding="utf-8") as file:
|
|
326
629
|
for i, line in enumerate(file, 1):
|
|
327
630
|
if i in lines_to_read:
|
|
328
631
|
partial_content.append(line)
|
|
329
632
|
|
|
330
|
-
return
|
|
633
|
+
return "".join(partial_content)
|
|
331
634
|
|
|
332
635
|
|
|
333
636
|
def collect_file_content_by_extension(prompt_data_path, extensions):
|
|
@@ -337,7 +640,7 @@ def collect_file_content_by_extension(prompt_data_path, extensions):
|
|
|
337
640
|
files = find_files_with_endings(prompt_data_path, [ext])
|
|
338
641
|
for file_name in files:
|
|
339
642
|
file_path = join(prompt_data_path, file_name)
|
|
340
|
-
if ext
|
|
643
|
+
if ext in [".prompt_givens.md", ".prompt_global_givens.md"]:
|
|
341
644
|
givens, image_data = load_givens(file_path)
|
|
342
645
|
combined_content += givens
|
|
343
646
|
image_data_list.extend(image_data)
|
|
@@ -347,93 +650,177 @@ def collect_file_content_by_extension(prompt_data_path, extensions):
|
|
|
347
650
|
|
|
348
651
|
|
|
349
652
|
def prepend_system_prompt(message_list):
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
653
|
+
try:
|
|
654
|
+
langfuse_prompt = LLMSingleton.get_instance().langfuse.get_prompt(
|
|
655
|
+
"ara-cli/system-prompt"
|
|
656
|
+
)
|
|
657
|
+
system_prompt = langfuse_prompt.prompt if langfuse_prompt.prompt else None
|
|
658
|
+
except (LangfuseError, NotFoundError, Exception) as e:
|
|
659
|
+
logging.info(f"Could not fetch Langfuse system prompt: {e}")
|
|
660
|
+
system_prompt = None
|
|
661
|
+
|
|
662
|
+
# Fallback to default prompt if Langfuse prompt is not available
|
|
663
|
+
if not system_prompt:
|
|
664
|
+
logging.info("Using default system prompt.")
|
|
665
|
+
system_prompt = "You are a helpful assistant that can process both text and images."
|
|
666
|
+
|
|
667
|
+
# Prepend the system prompt
|
|
668
|
+
system_prompt_message = {"role": "system", "content": system_prompt}
|
|
669
|
+
|
|
670
|
+
message_list.insert(0, system_prompt_message)
|
|
355
671
|
return message_list
|
|
356
672
|
|
|
357
673
|
|
|
358
674
|
def append_images_to_message(message, image_data_list):
|
|
675
|
+
"""
|
|
676
|
+
Appends image data list to a single message dict (NOT to a list).
|
|
677
|
+
"""
|
|
678
|
+
logger = logging.getLogger(__name__)
|
|
679
|
+
|
|
680
|
+
logger.debug(
|
|
681
|
+
f"append_images_to_message called with image_data_list length: {len(image_data_list) if image_data_list else 0}"
|
|
682
|
+
)
|
|
683
|
+
|
|
359
684
|
if not image_data_list:
|
|
685
|
+
logger.debug("No images to append, returning original message")
|
|
360
686
|
return message
|
|
361
|
-
|
|
362
|
-
message
|
|
687
|
+
|
|
688
|
+
message_content = message.get("content")
|
|
689
|
+
logger.debug(f"Original message content: {message_content}")
|
|
690
|
+
|
|
691
|
+
if isinstance(message_content, str):
|
|
692
|
+
message["content"] = [{"type": "text", "text": message_content}]
|
|
693
|
+
|
|
694
|
+
if isinstance(message["content"], list):
|
|
695
|
+
message["content"].extend(image_data_list)
|
|
696
|
+
else:
|
|
697
|
+
# If somehow content is not list or str, coerce to list
|
|
698
|
+
message["content"] = [{"type": "text", "text": str(message_content)}] + image_data_list
|
|
699
|
+
|
|
700
|
+
logger.debug(f"Updated message content with {len(image_data_list)} images")
|
|
363
701
|
|
|
364
702
|
return message
|
|
365
703
|
|
|
366
704
|
|
|
367
705
|
def create_and_send_custom_prompt(classifier, parameter):
|
|
368
706
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
369
|
-
prompt_data_path = f"ara/{sub_directory}/{parameter}.data/prompt.data"
|
|
707
|
+
prompt_data_path = _norm(f"ara/{sub_directory}/{parameter}.data/prompt.data")
|
|
370
708
|
prompt_file_path_markdown = join(prompt_data_path, f"{classifier}.prompt.md")
|
|
371
709
|
|
|
372
|
-
extensions = [
|
|
373
|
-
|
|
710
|
+
extensions = [
|
|
711
|
+
".blueprint.md",
|
|
712
|
+
".rules.md",
|
|
713
|
+
".prompt_givens.md",
|
|
714
|
+
".prompt_global_givens.md",
|
|
715
|
+
".intention.md",
|
|
716
|
+
".commands.md",
|
|
717
|
+
]
|
|
718
|
+
combined_content_markdown, image_data_list = collect_file_content_by_extension(
|
|
719
|
+
prompt_data_path, extensions
|
|
720
|
+
)
|
|
374
721
|
|
|
375
|
-
with open(prompt_file_path_markdown,
|
|
722
|
+
with open(prompt_file_path_markdown, "w", encoding="utf-8") as file:
|
|
376
723
|
file.write(combined_content_markdown)
|
|
377
724
|
|
|
378
725
|
prompt = read_string_from_file(prompt_file_path_markdown)
|
|
379
726
|
append_headings(classifier, parameter, "prompt")
|
|
380
727
|
write_prompt_result(classifier, parameter, prompt)
|
|
381
728
|
|
|
382
|
-
message
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
729
|
+
# Build message and append images correctly (fixed)
|
|
730
|
+
message = {"role": "user", "content": combined_content_markdown}
|
|
731
|
+
message = append_images_to_message(message, image_data_list)
|
|
387
732
|
message_list = [message]
|
|
388
733
|
|
|
389
|
-
message_list = append_images_to_message(message_list, image_data_list)
|
|
390
734
|
append_headings(classifier, parameter, "result")
|
|
391
735
|
|
|
392
|
-
artefact_data_path =
|
|
393
|
-
|
|
736
|
+
artefact_data_path = _norm(
|
|
737
|
+
f"ara/{sub_directory}/{parameter}.data/{classifier}.prompt_log.md"
|
|
738
|
+
)
|
|
739
|
+
with open(artefact_data_path, "a", encoding="utf-8") as file:
|
|
394
740
|
for chunk in send_prompt(message_list):
|
|
395
741
|
chunk_content = chunk.choices[0].delta.content
|
|
396
742
|
if not chunk_content:
|
|
397
743
|
continue
|
|
398
744
|
file.write(chunk_content)
|
|
399
745
|
file.flush()
|
|
400
|
-
# write_prompt_result(classifier, parameter, response)
|
|
401
746
|
|
|
402
747
|
|
|
403
|
-
def generate_config_prompt_template_file(
|
|
404
|
-
|
|
405
|
-
|
|
748
|
+
def generate_config_prompt_template_file(
|
|
749
|
+
prompt_data_path, config_prompt_templates_name
|
|
750
|
+
):
|
|
751
|
+
config_prompt_templates_path = os.path.join(
|
|
752
|
+
prompt_data_path, config_prompt_templates_name
|
|
753
|
+
)
|
|
754
|
+
# Use instance method consistently
|
|
755
|
+
config = ConfigManager().get_config()
|
|
406
756
|
global_prompt_template_path = TemplatePathManager.get_template_base_path()
|
|
407
|
-
dir_list = ["ara/.araconfig/custom-prompt-modules"] + [
|
|
408
|
-
|
|
757
|
+
dir_list = ["ara/.araconfig/custom-prompt-modules"] + [
|
|
758
|
+
f"{os.path.join(global_prompt_template_path,'prompt-modules')}"
|
|
759
|
+
]
|
|
760
|
+
file_list = ["*.blueprint.md", "*.rules.md", "*.intention.md", "*.commands.md"]
|
|
409
761
|
|
|
410
762
|
print(f"used {dir_list} for prompt templates file listing")
|
|
411
763
|
generate_markdown_listing(dir_list, file_list, config_prompt_templates_path)
|
|
412
764
|
|
|
413
765
|
|
|
414
|
-
def generate_config_prompt_givens_file(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
766
|
+
def generate_config_prompt_givens_file(
|
|
767
|
+
prompt_data_path, config_prompt_givens_name, artefact_to_mark=None
|
|
768
|
+
):
|
|
769
|
+
config_prompt_givens_path = os.path.join(
|
|
770
|
+
prompt_data_path, config_prompt_givens_name
|
|
771
|
+
)
|
|
772
|
+
config = ConfigManager().get_config()
|
|
773
|
+
dir_list = (
|
|
774
|
+
["ara"]
|
|
775
|
+
+ [path for d in config.ext_code_dirs for path in d.values()]
|
|
776
|
+
+ [config.doc_dir]
|
|
777
|
+
+ [config.glossary_dir]
|
|
778
|
+
)
|
|
418
779
|
|
|
419
780
|
print(f"used {dir_list} for prompt givens file listing")
|
|
420
|
-
generate_markdown_listing(
|
|
781
|
+
generate_markdown_listing(
|
|
782
|
+
dir_list, config.ara_prompt_given_list_includes, config_prompt_givens_path
|
|
783
|
+
)
|
|
421
784
|
|
|
422
785
|
# If an artefact is specified, mark it with [x]
|
|
423
786
|
if artefact_to_mark:
|
|
424
|
-
print(
|
|
787
|
+
print(
|
|
788
|
+
f"artefact {artefact_to_mark} marked in related config.prompt_givens.md per default"
|
|
789
|
+
)
|
|
425
790
|
|
|
426
791
|
# Read the generated file content
|
|
427
|
-
with open(config_prompt_givens_path,
|
|
792
|
+
with open(config_prompt_givens_path, "r", encoding="utf-8") as file:
|
|
428
793
|
markdown_listing = file.readlines()
|
|
429
794
|
|
|
430
795
|
updated_listing = []
|
|
431
796
|
for line in markdown_listing:
|
|
432
797
|
# Use a regular expression to match the exact string
|
|
433
|
-
if re.search(r
|
|
798
|
+
if re.search(r"\b" + re.escape(artefact_to_mark) + r"\b", line):
|
|
434
799
|
line = line.replace("[]", "[x]")
|
|
435
800
|
updated_listing.append(line)
|
|
436
801
|
|
|
437
802
|
# Write the updated listing back to the file
|
|
438
|
-
with open(config_prompt_givens_path,
|
|
803
|
+
with open(config_prompt_givens_path, "w", encoding="utf-8") as file:
|
|
439
804
|
file.write("".join(updated_listing))
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def generate_config_prompt_global_givens_file(
|
|
808
|
+
prompt_data_path, config_prompt_givens_name, artefact_to_mark=None
|
|
809
|
+
):
|
|
810
|
+
from ara_cli.global_file_lister import generate_global_markdown_listing
|
|
811
|
+
|
|
812
|
+
config_prompt_givens_path = os.path.join(
|
|
813
|
+
prompt_data_path, config_prompt_givens_name
|
|
814
|
+
)
|
|
815
|
+
config = ConfigManager().get_config()
|
|
816
|
+
|
|
817
|
+
if not hasattr(config, "global_dirs") or not config.global_dirs:
|
|
818
|
+
return
|
|
819
|
+
|
|
820
|
+
dir_list = [path for d in config.global_dirs for path in d.values()]
|
|
821
|
+
print(
|
|
822
|
+
f"used {dir_list} for global prompt givens file listing with absolute paths"
|
|
823
|
+
)
|
|
824
|
+
generate_global_markdown_listing(
|
|
825
|
+
dir_list, config.ara_prompt_given_list_includes, config_prompt_givens_path
|
|
826
|
+
)
|