abstractcore 2.4.2__py3-none-any.whl → 2.4.4__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.
- abstractcore/apps/app_config_utils.py +19 -0
- abstractcore/apps/summarizer.py +85 -56
- abstractcore/architectures/detection.py +15 -4
- abstractcore/assets/architecture_formats.json +1 -1
- abstractcore/assets/model_capabilities.json +420 -11
- abstractcore/core/interface.py +2 -0
- abstractcore/core/session.py +4 -0
- abstractcore/embeddings/manager.py +54 -16
- abstractcore/media/__init__.py +116 -148
- abstractcore/media/auto_handler.py +363 -0
- abstractcore/media/base.py +456 -0
- abstractcore/media/capabilities.py +335 -0
- abstractcore/media/types.py +300 -0
- abstractcore/media/vision_fallback.py +260 -0
- abstractcore/providers/anthropic_provider.py +18 -1
- abstractcore/providers/base.py +187 -0
- abstractcore/providers/huggingface_provider.py +111 -12
- abstractcore/providers/lmstudio_provider.py +88 -5
- abstractcore/providers/mlx_provider.py +33 -1
- abstractcore/providers/ollama_provider.py +37 -3
- abstractcore/providers/openai_provider.py +18 -1
- abstractcore/server/app.py +1390 -104
- abstractcore/tools/common_tools.py +12 -8
- abstractcore/utils/__init__.py +9 -5
- abstractcore/utils/cli.py +199 -17
- abstractcore/utils/message_preprocessor.py +182 -0
- abstractcore/utils/structured_logging.py +117 -16
- abstractcore/utils/version.py +1 -1
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/METADATA +214 -20
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/RECORD +34 -27
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/entry_points.txt +1 -0
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/WHEEL +0 -0
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/licenses/LICENSE +0 -0
- {abstractcore-2.4.2.dist-info → abstractcore-2.4.4.dist-info}/top_level.txt +0 -0
|
@@ -121,7 +121,8 @@ def list_files(directory_path: str = ".", pattern: str = "*", recursive: bool =
|
|
|
121
121
|
except ValueError:
|
|
122
122
|
head_limit = 50 # fallback to default
|
|
123
123
|
|
|
124
|
-
directory
|
|
124
|
+
# Expand home directory shortcuts like ~
|
|
125
|
+
directory = Path(directory_path).expanduser()
|
|
125
126
|
|
|
126
127
|
if not directory.exists():
|
|
127
128
|
return f"Error: Directory '{directory_path}' does not exist"
|
|
@@ -302,7 +303,8 @@ def search_files(
|
|
|
302
303
|
except ValueError:
|
|
303
304
|
head_limit = 20 # fallback to default
|
|
304
305
|
|
|
305
|
-
|
|
306
|
+
# Expand home directory shortcuts like ~
|
|
307
|
+
search_path = Path(path).expanduser()
|
|
306
308
|
|
|
307
309
|
# Compile regex pattern
|
|
308
310
|
flags = 0 if case_sensitive else re.IGNORECASE
|
|
@@ -651,7 +653,8 @@ def read_file(file_path: str, should_read_entire_file: bool = True, start_line_o
|
|
|
651
653
|
File contents or error message
|
|
652
654
|
"""
|
|
653
655
|
try:
|
|
654
|
-
|
|
656
|
+
# Expand home directory shortcuts like ~
|
|
657
|
+
path = Path(file_path).expanduser()
|
|
655
658
|
|
|
656
659
|
if not path.exists():
|
|
657
660
|
return f"Error: File '{file_path}' does not exist"
|
|
@@ -765,8 +768,8 @@ def write_file(file_path: str, content: str = "", mode: str = "w", create_dirs:
|
|
|
765
768
|
OSError: If there are filesystem issues
|
|
766
769
|
"""
|
|
767
770
|
try:
|
|
768
|
-
# Convert to Path object for better handling
|
|
769
|
-
path = Path(file_path)
|
|
771
|
+
# Convert to Path object for better handling and expand home directory shortcuts like ~
|
|
772
|
+
path = Path(file_path).expanduser()
|
|
770
773
|
|
|
771
774
|
# Create parent directories if requested and they don't exist
|
|
772
775
|
if create_dirs and path.parent != path:
|
|
@@ -1073,8 +1076,8 @@ def edit_file(
|
|
|
1073
1076
|
edit_file("test.py", "class OldClass", "class NewClass", preview_only=True)
|
|
1074
1077
|
"""
|
|
1075
1078
|
try:
|
|
1076
|
-
# Validate file exists
|
|
1077
|
-
path = Path(file_path)
|
|
1079
|
+
# Validate file exists and expand home directory shortcuts like ~
|
|
1080
|
+
path = Path(file_path).expanduser()
|
|
1078
1081
|
if not path.exists():
|
|
1079
1082
|
return f"❌ File not found: {file_path}"
|
|
1080
1083
|
|
|
@@ -1344,7 +1347,8 @@ def execute_command(
|
|
|
1344
1347
|
|
|
1345
1348
|
# Working directory validation
|
|
1346
1349
|
if working_directory:
|
|
1347
|
-
|
|
1350
|
+
# Expand home directory shortcuts like ~ before resolving
|
|
1351
|
+
working_dir = Path(working_directory).expanduser().resolve()
|
|
1348
1352
|
if not working_dir.exists():
|
|
1349
1353
|
return f"❌ Error: Working directory does not exist: {working_directory}"
|
|
1350
1354
|
if not working_dir.is_dir():
|
abstractcore/utils/__init__.py
CHANGED
|
@@ -5,13 +5,14 @@ Utility functions for AbstractCore.
|
|
|
5
5
|
from .structured_logging import configure_logging, get_logger, capture_session
|
|
6
6
|
from .version import __version__
|
|
7
7
|
from .token_utils import (
|
|
8
|
-
TokenUtils,
|
|
9
|
-
count_tokens,
|
|
10
|
-
estimate_tokens,
|
|
8
|
+
TokenUtils,
|
|
9
|
+
count_tokens,
|
|
10
|
+
estimate_tokens,
|
|
11
11
|
count_tokens_precise,
|
|
12
12
|
TokenCountMethod,
|
|
13
13
|
ContentType
|
|
14
14
|
)
|
|
15
|
+
from .message_preprocessor import MessagePreprocessor, parse_files, has_files
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
'configure_logging',
|
|
@@ -20,8 +21,11 @@ __all__ = [
|
|
|
20
21
|
'__version__',
|
|
21
22
|
'TokenUtils',
|
|
22
23
|
'count_tokens',
|
|
23
|
-
'estimate_tokens',
|
|
24
|
+
'estimate_tokens',
|
|
24
25
|
'count_tokens_precise',
|
|
25
26
|
'TokenCountMethod',
|
|
26
|
-
'ContentType'
|
|
27
|
+
'ContentType',
|
|
28
|
+
'MessagePreprocessor',
|
|
29
|
+
'parse_files',
|
|
30
|
+
'has_files'
|
|
27
31
|
]
|
abstractcore/utils/cli.py
CHANGED
|
@@ -25,6 +25,18 @@ import sys
|
|
|
25
25
|
import time
|
|
26
26
|
from typing import Optional
|
|
27
27
|
|
|
28
|
+
# Enable command history and arrow key navigation
|
|
29
|
+
try:
|
|
30
|
+
import readline
|
|
31
|
+
# Configure readline for better history behavior
|
|
32
|
+
readline.set_startup_hook(lambda: readline.insert_text(''))
|
|
33
|
+
readline.parse_and_bind("tab: complete")
|
|
34
|
+
# Set a reasonable history length
|
|
35
|
+
readline.set_history_length(1000)
|
|
36
|
+
except ImportError:
|
|
37
|
+
# readline not available (typically on Windows)
|
|
38
|
+
readline = None
|
|
39
|
+
|
|
28
40
|
from .. import create_llm, BasicSession
|
|
29
41
|
from ..tools.common_tools import list_files, read_file, write_file, execute_command, search_files
|
|
30
42
|
from ..processing import BasicExtractor, BasicJudge
|
|
@@ -57,11 +69,14 @@ class SimpleCLI:
|
|
|
57
69
|
|
|
58
70
|
self.max_tokens = max_tokens
|
|
59
71
|
|
|
72
|
+
# Initialize command history with persistent storage
|
|
73
|
+
self._setup_command_history()
|
|
74
|
+
|
|
60
75
|
# Initialize provider and session with tools
|
|
61
76
|
self.provider = create_llm(provider, model=model, max_tokens=max_tokens, **kwargs)
|
|
62
77
|
self.session = BasicSession(
|
|
63
78
|
self.provider,
|
|
64
|
-
system_prompt="You are a helpful AI assistant.",
|
|
79
|
+
system_prompt="You are a helpful AI assistant with vision capabilities. When users provide images or media files, analyze and describe them directly. You also have access to file operation tools.",
|
|
65
80
|
tools=[list_files, read_file, write_file, execute_command, search_files]
|
|
66
81
|
)
|
|
67
82
|
|
|
@@ -81,6 +96,47 @@ class SimpleCLI:
|
|
|
81
96
|
print("💡 Ask questions naturally or use tools: 'What files are here?'")
|
|
82
97
|
print("=" * 70)
|
|
83
98
|
|
|
99
|
+
def _setup_command_history(self):
|
|
100
|
+
"""Setup command history with persistent storage."""
|
|
101
|
+
if readline is None:
|
|
102
|
+
return # No readline support available
|
|
103
|
+
|
|
104
|
+
# Store history in user's home directory
|
|
105
|
+
import os
|
|
106
|
+
import pathlib
|
|
107
|
+
|
|
108
|
+
# Create .abstractcore directory if it doesn't exist
|
|
109
|
+
history_dir = pathlib.Path.home() / '.abstractcore'
|
|
110
|
+
history_dir.mkdir(exist_ok=True)
|
|
111
|
+
|
|
112
|
+
# Define history file path
|
|
113
|
+
self.history_file = history_dir / 'cli_history.txt'
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Load existing history if file exists
|
|
117
|
+
if self.history_file.exists():
|
|
118
|
+
readline.read_history_file(str(self.history_file))
|
|
119
|
+
if self.debug_mode:
|
|
120
|
+
history_size = readline.get_current_history_length()
|
|
121
|
+
print(f"🔍 Loaded {history_size} command(s) from history")
|
|
122
|
+
except (FileNotFoundError, PermissionError) as e:
|
|
123
|
+
if self.debug_mode:
|
|
124
|
+
print(f"⚠️ Could not load command history: {e}")
|
|
125
|
+
|
|
126
|
+
def _save_command_history(self):
|
|
127
|
+
"""Save current command history to disk."""
|
|
128
|
+
if readline is None or not hasattr(self, 'history_file'):
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Ensure the directory exists
|
|
133
|
+
self.history_file.parent.mkdir(exist_ok=True)
|
|
134
|
+
# Save history to file
|
|
135
|
+
readline.write_history_file(str(self.history_file))
|
|
136
|
+
except (PermissionError, OSError) as e:
|
|
137
|
+
if self.debug_mode:
|
|
138
|
+
print(f"⚠️ Could not save command history: {e}")
|
|
139
|
+
|
|
84
140
|
def handle_command(self, user_input: str) -> bool:
|
|
85
141
|
"""Handle commands. Returns True if command processed, False otherwise."""
|
|
86
142
|
if not user_input.startswith('/'):
|
|
@@ -89,6 +145,7 @@ class SimpleCLI:
|
|
|
89
145
|
cmd = user_input[1:].strip()
|
|
90
146
|
|
|
91
147
|
if cmd in ['quit', 'exit', 'q']:
|
|
148
|
+
self._save_command_history()
|
|
92
149
|
print("👋 Goodbye!")
|
|
93
150
|
sys.exit(0)
|
|
94
151
|
|
|
@@ -101,7 +158,8 @@ class SimpleCLI:
|
|
|
101
158
|
print("─" * 50)
|
|
102
159
|
print(" /help Show this comprehensive help")
|
|
103
160
|
print(" /quit Exit the CLI")
|
|
104
|
-
print(" /clear Clear
|
|
161
|
+
print(" /clear Clear the screen (like unix terminal)")
|
|
162
|
+
print(" /reset Reset conversation history")
|
|
105
163
|
print(" /status Show system status and capabilities")
|
|
106
164
|
|
|
107
165
|
print("\n💬 CONVERSATION MANAGEMENT")
|
|
@@ -153,11 +211,22 @@ class SimpleCLI:
|
|
|
153
211
|
print(" • write_file Create or modify files")
|
|
154
212
|
print(" • execute_command Run shell commands")
|
|
155
213
|
|
|
214
|
+
print("\n📎 FILE ATTACHMENTS")
|
|
215
|
+
print("─" * 50)
|
|
216
|
+
print(" Use @filename syntax to attach files to your message:")
|
|
217
|
+
print(" • Images: 'Analyze this screenshot @screenshot.png'")
|
|
218
|
+
print(" • Documents: 'Summarize @report.pdf and @data.csv'")
|
|
219
|
+
print(" • Multiple files: 'Compare @image1.jpg @image2.jpg @notes.txt'")
|
|
220
|
+
print(" • Vision analysis: Works with vision models (GPT-4o, Claude, qwen2.5vl)")
|
|
221
|
+
print(" • Auto-fallback: Text-only models use vision captioning for images")
|
|
222
|
+
print(" • Supported formats: Images (jpg, png, gif), PDFs, Office docs, text files")
|
|
223
|
+
|
|
156
224
|
print("\n💡 TIPS & EXAMPLES")
|
|
157
225
|
print("─" * 50)
|
|
158
226
|
print(" • Ask questions naturally: 'What files are in this directory?'")
|
|
159
227
|
print(" • Search inside files: 'Find all TODO comments in Python files'")
|
|
160
228
|
print(" • Request file operations: 'Read the README.md file'")
|
|
229
|
+
print(" • Attach files: 'What's in this image? @photo.jpg'")
|
|
161
230
|
print(" • Save important conversations: '/save project_discussion --summary'")
|
|
162
231
|
print(" • Switch models for different tasks: '/model ollama:qwen3-coder:30b'")
|
|
163
232
|
print(" • Use /status to check token usage and model capabilities")
|
|
@@ -167,8 +236,13 @@ class SimpleCLI:
|
|
|
167
236
|
print("=" * 70 + "\n")
|
|
168
237
|
|
|
169
238
|
elif cmd == 'clear':
|
|
239
|
+
# Clear the screen like in unix terminal
|
|
240
|
+
import os
|
|
241
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
242
|
+
|
|
243
|
+
elif cmd == 'reset':
|
|
170
244
|
self.session.clear_history(keep_system=True)
|
|
171
|
-
print("🧹
|
|
245
|
+
print("🧹 Chat history reset")
|
|
172
246
|
|
|
173
247
|
elif cmd == 'stream':
|
|
174
248
|
self.stream_mode = not self.stream_mode
|
|
@@ -208,7 +282,7 @@ class SimpleCLI:
|
|
|
208
282
|
max_tokens=self.max_tokens, **self.kwargs)
|
|
209
283
|
self.session = BasicSession(
|
|
210
284
|
self.provider,
|
|
211
|
-
system_prompt="You are a helpful AI assistant.",
|
|
285
|
+
system_prompt="You are a helpful AI assistant with vision capabilities. When users provide images or media files, analyze and describe them directly. You also have access to file operation tools.",
|
|
212
286
|
tools=[list_files, read_file, write_file, execute_command, search_files]
|
|
213
287
|
)
|
|
214
288
|
print("✅ Model switched")
|
|
@@ -898,18 +972,62 @@ class SimpleCLI:
|
|
|
898
972
|
|
|
899
973
|
print("=" * 60)
|
|
900
974
|
|
|
975
|
+
def _parse_file_attachments(self, user_input: str):
|
|
976
|
+
"""Parse @filename references using AbstractCore's message preprocessor."""
|
|
977
|
+
from ..utils.message_preprocessor import MessagePreprocessor
|
|
978
|
+
import os
|
|
979
|
+
|
|
980
|
+
# Use AbstractCore's centralized file parsing logic
|
|
981
|
+
clean_input, media_files = MessagePreprocessor.parse_file_attachments(
|
|
982
|
+
user_input,
|
|
983
|
+
validate_existence=True,
|
|
984
|
+
verbose=self.debug_mode
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
# Show user-friendly status messages for CLI (only in interactive mode)
|
|
988
|
+
if media_files and not self.single_prompt_mode:
|
|
989
|
+
print(f"📎 Attaching {len(media_files)} file(s): {', '.join(media_files)}")
|
|
990
|
+
|
|
991
|
+
# Check for vision capabilities if images are attached
|
|
992
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
|
|
993
|
+
image_files = [f for f in media_files if os.path.splitext(f.lower())[1] in image_extensions]
|
|
994
|
+
|
|
995
|
+
if image_files:
|
|
996
|
+
try:
|
|
997
|
+
from ..media.capabilities import is_vision_model
|
|
998
|
+
if is_vision_model(self.model_name):
|
|
999
|
+
print(f"👁️ Vision model detected - will analyze {len(image_files)} image(s)")
|
|
1000
|
+
else:
|
|
1001
|
+
print(f"📷 Text model - will use vision fallback for {len(image_files)} image(s)")
|
|
1002
|
+
except:
|
|
1003
|
+
print(f"📷 Processing {len(image_files)} image(s)")
|
|
1004
|
+
|
|
1005
|
+
return clean_input, media_files
|
|
1006
|
+
|
|
901
1007
|
def generate_response(self, user_input: str):
|
|
902
|
-
"""Generate and display response with tool execution."""
|
|
1008
|
+
"""Generate and display response with tool execution and file attachment support."""
|
|
903
1009
|
import re
|
|
904
1010
|
start_time = time.time()
|
|
905
1011
|
|
|
906
1012
|
try:
|
|
1013
|
+
# Parse @filename attachments
|
|
1014
|
+
clean_input, media_files = self._parse_file_attachments(user_input)
|
|
1015
|
+
|
|
1016
|
+
# If no text remains after removing file references, provide default prompt
|
|
1017
|
+
if not clean_input and media_files:
|
|
1018
|
+
clean_input = "Please analyze the attached file(s)."
|
|
1019
|
+
|
|
907
1020
|
if self.debug_mode:
|
|
908
1021
|
print(f"🔍 Sending to {self.provider_name}:{self.model_name}")
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
response
|
|
1022
|
+
if media_files:
|
|
1023
|
+
print(f"🔍 Media files: {media_files}")
|
|
1024
|
+
|
|
1025
|
+
# Generate response with media support
|
|
1026
|
+
response = self.session.generate(
|
|
1027
|
+
clean_input,
|
|
1028
|
+
stream=self.stream_mode,
|
|
1029
|
+
media=media_files if media_files else None
|
|
1030
|
+
)
|
|
913
1031
|
|
|
914
1032
|
if self.stream_mode:
|
|
915
1033
|
if not self.single_prompt_mode:
|
|
@@ -1180,10 +1298,12 @@ class SimpleCLI:
|
|
|
1180
1298
|
print("\n\n👋 Use /quit to exit.")
|
|
1181
1299
|
continue
|
|
1182
1300
|
except EOFError:
|
|
1301
|
+
self._save_command_history()
|
|
1183
1302
|
print("\n👋 Goodbye!")
|
|
1184
1303
|
break
|
|
1185
1304
|
|
|
1186
1305
|
except Exception as e:
|
|
1306
|
+
self._save_command_history()
|
|
1187
1307
|
print(f"❌ Fatal error: {e}")
|
|
1188
1308
|
|
|
1189
1309
|
def run_single_prompt(self, prompt: str):
|
|
@@ -1205,7 +1325,7 @@ Examples:
|
|
|
1205
1325
|
python -m abstractcore.utils.cli --provider ollama --model qwen3-coder:30b
|
|
1206
1326
|
python -m abstractcore.utils.cli --provider openai --model gpt-4o-mini --stream
|
|
1207
1327
|
python -m abstractcore.utils.cli --provider anthropic --model claude-3-5-haiku-20241022
|
|
1208
|
-
python -m abstractcore.utils.cli --
|
|
1328
|
+
python -m abstractcore.utils.cli --prompt "What is Python?" # Uses configured defaults
|
|
1209
1329
|
|
|
1210
1330
|
Key Commands:
|
|
1211
1331
|
/help Show comprehensive command guide
|
|
@@ -1221,17 +1341,26 @@ Key Commands:
|
|
|
1221
1341
|
|
|
1222
1342
|
Tools: list_files, search_files, read_file, write_file, execute_command
|
|
1223
1343
|
|
|
1344
|
+
File Attachments:
|
|
1345
|
+
Use @filename syntax to attach files: "Analyze @image.jpg and @doc.pdf"
|
|
1346
|
+
Supports images, PDFs, Office docs, text files with automatic processing
|
|
1347
|
+
Vision models analyze images directly; text models use vision fallback
|
|
1348
|
+
|
|
1349
|
+
Configuration:
|
|
1350
|
+
Set defaults with: abstractcore --set-app-default cli <provider> <model>
|
|
1351
|
+
Check status with: abstractcore --status
|
|
1352
|
+
|
|
1224
1353
|
Note: This is a basic demonstrator with limited capabilities. For production
|
|
1225
1354
|
use cases requiring advanced reasoning, ReAct patterns, or complex tool chains,
|
|
1226
1355
|
build custom solutions using the AbstractCore framework directly.
|
|
1227
1356
|
"""
|
|
1228
1357
|
)
|
|
1229
1358
|
|
|
1230
|
-
#
|
|
1231
|
-
parser.add_argument('--provider',
|
|
1359
|
+
# Optional arguments (no longer required - will use configured defaults)
|
|
1360
|
+
parser.add_argument('--provider',
|
|
1232
1361
|
choices=['openai', 'anthropic', 'ollama', 'huggingface', 'mlx', 'lmstudio'],
|
|
1233
|
-
help='LLM provider to use')
|
|
1234
|
-
parser.add_argument('--model',
|
|
1362
|
+
help='LLM provider to use (optional - uses configured default)')
|
|
1363
|
+
parser.add_argument('--model', help='Model name to use (optional - uses configured default)')
|
|
1235
1364
|
|
|
1236
1365
|
# Optional arguments
|
|
1237
1366
|
parser.add_argument('--stream', action='store_true', help='Enable streaming mode')
|
|
@@ -1246,6 +1375,59 @@ build custom solutions using the AbstractCore framework directly.
|
|
|
1246
1375
|
|
|
1247
1376
|
args = parser.parse_args()
|
|
1248
1377
|
|
|
1378
|
+
# Load configuration manager for defaults
|
|
1379
|
+
try:
|
|
1380
|
+
from ..config import get_config_manager
|
|
1381
|
+
config_manager = get_config_manager()
|
|
1382
|
+
except Exception as e:
|
|
1383
|
+
config_manager = None
|
|
1384
|
+
if not args.provider or not args.model:
|
|
1385
|
+
print(f"❌ Error loading configuration: {e}")
|
|
1386
|
+
print("💡 Please specify --provider and --model explicitly")
|
|
1387
|
+
sys.exit(1)
|
|
1388
|
+
|
|
1389
|
+
# Get provider and model from configuration if not specified
|
|
1390
|
+
if not args.provider or not args.model:
|
|
1391
|
+
if config_manager:
|
|
1392
|
+
default_provider, default_model = config_manager.get_app_default('cli')
|
|
1393
|
+
|
|
1394
|
+
# Use configured defaults if available
|
|
1395
|
+
provider = args.provider or default_provider
|
|
1396
|
+
model = args.model or default_model
|
|
1397
|
+
|
|
1398
|
+
if not provider or not model:
|
|
1399
|
+
print("❌ Error: No provider/model specified and no defaults configured")
|
|
1400
|
+
print()
|
|
1401
|
+
print("💡 Solutions:")
|
|
1402
|
+
print(" 1. Specify explicitly: --provider ollama --model gemma3:1b-it-qat")
|
|
1403
|
+
print(" 2. Configure defaults: abstractcore --set-app-default cli ollama gemma3:1b-it-qat")
|
|
1404
|
+
print(" 3. Check current config: abstractcore --status")
|
|
1405
|
+
sys.exit(1)
|
|
1406
|
+
|
|
1407
|
+
# Show what we're using if defaults were applied
|
|
1408
|
+
if not args.provider or not args.model:
|
|
1409
|
+
if not args.prompt: # Only show in interactive mode
|
|
1410
|
+
print(f"🔧 Using configured defaults: {provider}/{model}")
|
|
1411
|
+
print(" (Configure with: abstractcore --set-app-default cli <provider> <model>)")
|
|
1412
|
+
print()
|
|
1413
|
+
else:
|
|
1414
|
+
print("❌ Error: No provider/model specified and configuration unavailable")
|
|
1415
|
+
sys.exit(1)
|
|
1416
|
+
else:
|
|
1417
|
+
# Use explicit arguments
|
|
1418
|
+
provider = args.provider
|
|
1419
|
+
model = args.model
|
|
1420
|
+
|
|
1421
|
+
# Get streaming default from configuration (only if --stream not explicitly provided)
|
|
1422
|
+
if not args.stream and config_manager:
|
|
1423
|
+
try:
|
|
1424
|
+
default_streaming = config_manager.get_streaming_default('cli')
|
|
1425
|
+
stream_mode = default_streaming
|
|
1426
|
+
except Exception:
|
|
1427
|
+
stream_mode = False # Safe fallback
|
|
1428
|
+
else:
|
|
1429
|
+
stream_mode = args.stream
|
|
1430
|
+
|
|
1249
1431
|
# Build kwargs
|
|
1250
1432
|
kwargs = {'temperature': args.temperature}
|
|
1251
1433
|
if args.base_url:
|
|
@@ -1255,9 +1437,9 @@ build custom solutions using the AbstractCore framework directly.
|
|
|
1255
1437
|
|
|
1256
1438
|
# Create CLI (suppress banner for single-prompt mode)
|
|
1257
1439
|
cli = SimpleCLI(
|
|
1258
|
-
provider=
|
|
1259
|
-
model=
|
|
1260
|
-
stream=
|
|
1440
|
+
provider=provider,
|
|
1441
|
+
model=model,
|
|
1442
|
+
stream=stream_mode,
|
|
1261
1443
|
max_tokens=args.max_tokens,
|
|
1262
1444
|
debug=args.debug,
|
|
1263
1445
|
show_banner=not args.prompt, # Hide banner in single-prompt mode
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message preprocessing utilities for AbstractCore.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for parsing and preprocessing user messages,
|
|
5
|
+
particularly for extracting file references using @filename syntax.
|
|
6
|
+
Used across all AbstractCore applications for consistent behavior.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import os
|
|
11
|
+
from typing import Tuple, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MessagePreprocessor:
|
|
15
|
+
"""
|
|
16
|
+
Message preprocessing utilities for extracting file references and cleaning input.
|
|
17
|
+
|
|
18
|
+
Supports @filename syntax for attaching files to messages across all AbstractCore apps.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Pattern to match @filename (supports various file extensions)
|
|
22
|
+
FILE_PATTERN = r'@([^\s@]+\.[\w]+)'
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def parse_file_attachments(user_input: str,
|
|
26
|
+
validate_existence: bool = True,
|
|
27
|
+
verbose: bool = False) -> Tuple[str, List[str]]:
|
|
28
|
+
"""
|
|
29
|
+
Parse @filename references from user input and return cleaned text + file list.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
user_input: The user's message that may contain @filename references
|
|
33
|
+
validate_existence: Whether to check if files actually exist (default: True)
|
|
34
|
+
verbose: Whether to log file processing details (default: False)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (clean_input_text, list_of_valid_file_paths)
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> clean_text, files = MessagePreprocessor.parse_file_attachments(
|
|
41
|
+
... "Analyze this image @screenshot.png and @data.csv"
|
|
42
|
+
... )
|
|
43
|
+
>>> print(clean_text)
|
|
44
|
+
"Analyze this image and"
|
|
45
|
+
>>> print(files)
|
|
46
|
+
["screenshot.png", "data.csv"]
|
|
47
|
+
"""
|
|
48
|
+
# Find all @filename references
|
|
49
|
+
matches = re.findall(MessagePreprocessor.FILE_PATTERN, user_input)
|
|
50
|
+
|
|
51
|
+
if not matches:
|
|
52
|
+
return user_input, []
|
|
53
|
+
|
|
54
|
+
valid_files = []
|
|
55
|
+
invalid_files = []
|
|
56
|
+
|
|
57
|
+
for filename in matches:
|
|
58
|
+
if not validate_existence or os.path.exists(filename):
|
|
59
|
+
valid_files.append(filename)
|
|
60
|
+
if verbose:
|
|
61
|
+
size_kb = os.path.getsize(filename) / 1024 if validate_existence else 0
|
|
62
|
+
print(f"📎 Found file: {filename} ({size_kb:.1f}KB)")
|
|
63
|
+
else:
|
|
64
|
+
invalid_files.append(filename)
|
|
65
|
+
|
|
66
|
+
# Show warnings for missing files if verbose mode
|
|
67
|
+
if verbose and invalid_files:
|
|
68
|
+
print(f"⚠️ Files not found: {', '.join(invalid_files)}")
|
|
69
|
+
|
|
70
|
+
# Remove @filename references from the input text
|
|
71
|
+
clean_input = re.sub(MessagePreprocessor.FILE_PATTERN, '', user_input)
|
|
72
|
+
|
|
73
|
+
# Clean up extra whitespace
|
|
74
|
+
clean_input = re.sub(r'\s+', ' ', clean_input).strip()
|
|
75
|
+
|
|
76
|
+
return clean_input, valid_files
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def has_file_attachments(user_input: str) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Check if the user input contains any @filename references.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
user_input: The message to check
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if @filename patterns are found, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
return bool(re.search(MessagePreprocessor.FILE_PATTERN, user_input))
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def get_file_count(user_input: str) -> int:
|
|
93
|
+
"""
|
|
94
|
+
Count the number of @filename references in the input.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
user_input: The message to analyze
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Number of @filename patterns found
|
|
101
|
+
"""
|
|
102
|
+
return len(re.findall(MessagePreprocessor.FILE_PATTERN, user_input))
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def extract_file_paths(user_input: str) -> List[str]:
|
|
106
|
+
"""
|
|
107
|
+
Extract just the file paths from @filename references without validation.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
user_input: The message containing @filename references
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of file paths (may include non-existent files)
|
|
114
|
+
"""
|
|
115
|
+
return re.findall(MessagePreprocessor.FILE_PATTERN, user_input)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def process_message_with_media(user_input: str,
|
|
119
|
+
default_prompt: Optional[str] = None,
|
|
120
|
+
validate_files: bool = True,
|
|
121
|
+
verbose: bool = False) -> Tuple[str, List[str]]:
|
|
122
|
+
"""
|
|
123
|
+
Process a message with @filename attachments, providing a default prompt if needed.
|
|
124
|
+
|
|
125
|
+
This is the main entry point for applications that want full message preprocessing.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
user_input: The user's message
|
|
129
|
+
default_prompt: Default text to use if only files are specified (e.g., "Analyze the attached files")
|
|
130
|
+
validate_files: Whether to validate file existence
|
|
131
|
+
verbose: Whether to show processing details
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple of (processed_prompt, media_file_list)
|
|
135
|
+
"""
|
|
136
|
+
clean_input, media_files = MessagePreprocessor.parse_file_attachments(
|
|
137
|
+
user_input,
|
|
138
|
+
validate_existence=validate_files,
|
|
139
|
+
verbose=verbose
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# If no text remains after removing file references, use default prompt
|
|
143
|
+
if not clean_input and media_files and default_prompt:
|
|
144
|
+
clean_input = default_prompt
|
|
145
|
+
|
|
146
|
+
return clean_input, media_files
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Convenience functions for common use cases
|
|
150
|
+
def parse_files(user_input: str, verbose: bool = False) -> Tuple[str, List[str]]:
|
|
151
|
+
"""
|
|
152
|
+
Convenience function for basic file parsing.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
user_input: Message with @filename references
|
|
156
|
+
verbose: Show processing details
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (clean_text, file_list)
|
|
160
|
+
"""
|
|
161
|
+
return MessagePreprocessor.parse_file_attachments(user_input, verbose=verbose)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def has_files(user_input: str) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Convenience function to check if message has file attachments.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
user_input: Message to check
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if @filename patterns found
|
|
173
|
+
"""
|
|
174
|
+
return MessagePreprocessor.has_file_attachments(user_input)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Export main classes and functions
|
|
178
|
+
__all__ = [
|
|
179
|
+
'MessagePreprocessor',
|
|
180
|
+
'parse_files',
|
|
181
|
+
'has_files'
|
|
182
|
+
]
|