aiecs 1.7.6__py3-none-any.whl → 1.8.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.
Potentially problematic release.
This version of aiecs might be problematic. Click here for more details.
- aiecs/__init__.py +1 -1
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +5 -1
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +7 -5
- aiecs/config/config.py +3 -0
- aiecs/config/tool_config.py +55 -19
- aiecs/domain/agent/base_agent.py +79 -0
- aiecs/domain/agent/hybrid_agent.py +552 -175
- aiecs/domain/agent/knowledge_aware_agent.py +3 -2
- aiecs/domain/agent/llm_agent.py +2 -0
- aiecs/domain/agent/models.py +10 -0
- aiecs/domain/agent/tools/schema_generator.py +17 -4
- aiecs/llm/callbacks/custom_callbacks.py +9 -4
- aiecs/llm/client_factory.py +20 -7
- aiecs/llm/clients/base_client.py +50 -5
- aiecs/llm/clients/google_function_calling_mixin.py +46 -88
- aiecs/llm/clients/googleai_client.py +183 -9
- aiecs/llm/clients/openai_client.py +12 -0
- aiecs/llm/clients/openai_compatible_mixin.py +42 -2
- aiecs/llm/clients/openrouter_client.py +272 -0
- aiecs/llm/clients/vertex_client.py +385 -22
- aiecs/llm/clients/xai_client.py +41 -3
- aiecs/llm/protocols.py +19 -1
- aiecs/llm/utils/image_utils.py +179 -0
- aiecs/main.py +2 -2
- aiecs/tools/docs/document_creator_tool.py +143 -2
- aiecs/tools/docs/document_parser_tool.py +9 -4
- aiecs/tools/docs/document_writer_tool.py +179 -0
- aiecs/tools/task_tools/image_tool.py +49 -14
- aiecs/tools/task_tools/scraper_tool.py +39 -2
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/METADATA +4 -2
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/RECORD +35 -33
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/WHEEL +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/entry_points.txt +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/top_level.txt +0 -0
aiecs/llm/protocols.py
CHANGED
|
@@ -4,7 +4,7 @@ LLM Client Protocols
|
|
|
4
4
|
Defines Protocol interfaces for LLM clients to enable duck typing and flexible integration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Protocol, List, Optional, AsyncGenerator, runtime_checkable
|
|
7
|
+
from typing import Protocol, List, Optional, AsyncGenerator, runtime_checkable, Dict, Any
|
|
8
8
|
from aiecs.llm.clients.base_client import LLMMessage, LLMResponse
|
|
9
9
|
|
|
10
10
|
|
|
@@ -31,9 +31,12 @@ class LLMClientProtocol(Protocol):
|
|
|
31
31
|
model: Optional[str] = None,
|
|
32
32
|
temperature: float = 0.7,
|
|
33
33
|
max_tokens: Optional[int] = None,
|
|
34
|
+
context: Optional[Dict[str, Any]] = None,
|
|
34
35
|
**kwargs
|
|
35
36
|
) -> LLMResponse:
|
|
36
37
|
# Custom implementation
|
|
38
|
+
# Use context for tracking, billing, observability, etc.
|
|
39
|
+
user_id = context.get("user_id") if context else None
|
|
37
40
|
pass
|
|
38
41
|
|
|
39
42
|
async def stream_text(
|
|
@@ -42,6 +45,7 @@ class LLMClientProtocol(Protocol):
|
|
|
42
45
|
model: Optional[str] = None,
|
|
43
46
|
temperature: float = 0.7,
|
|
44
47
|
max_tokens: Optional[int] = None,
|
|
48
|
+
context: Optional[Dict[str, Any]] = None,
|
|
45
49
|
**kwargs
|
|
46
50
|
) -> AsyncGenerator[str, None]:
|
|
47
51
|
# Custom implementation
|
|
@@ -67,6 +71,7 @@ class LLMClientProtocol(Protocol):
|
|
|
67
71
|
model: Optional[str] = None,
|
|
68
72
|
temperature: float = 0.7,
|
|
69
73
|
max_tokens: Optional[int] = None,
|
|
74
|
+
context: Optional[Dict[str, Any]] = None,
|
|
70
75
|
**kwargs,
|
|
71
76
|
) -> LLMResponse:
|
|
72
77
|
"""
|
|
@@ -77,6 +82,12 @@ class LLMClientProtocol(Protocol):
|
|
|
77
82
|
model: Model name (optional, uses default if not provided)
|
|
78
83
|
temperature: Sampling temperature (0.0 to 1.0)
|
|
79
84
|
max_tokens: Maximum tokens to generate
|
|
85
|
+
context: Optional context dictionary containing metadata such as:
|
|
86
|
+
- user_id: User identifier for tracking/billing
|
|
87
|
+
- tenant_id: Tenant identifier for multi-tenant setups
|
|
88
|
+
- request_id: Request identifier for tracing
|
|
89
|
+
- session_id: Session identifier
|
|
90
|
+
- Any other custom metadata for observability or middleware
|
|
80
91
|
**kwargs: Additional provider-specific parameters
|
|
81
92
|
|
|
82
93
|
Returns:
|
|
@@ -90,6 +101,7 @@ class LLMClientProtocol(Protocol):
|
|
|
90
101
|
model: Optional[str] = None,
|
|
91
102
|
temperature: float = 0.7,
|
|
92
103
|
max_tokens: Optional[int] = None,
|
|
104
|
+
context: Optional[Dict[str, Any]] = None,
|
|
93
105
|
**kwargs,
|
|
94
106
|
) -> AsyncGenerator[str, None]:
|
|
95
107
|
"""
|
|
@@ -100,6 +112,12 @@ class LLMClientProtocol(Protocol):
|
|
|
100
112
|
model: Model name (optional, uses default if not provided)
|
|
101
113
|
temperature: Sampling temperature (0.0 to 1.0)
|
|
102
114
|
max_tokens: Maximum tokens to generate
|
|
115
|
+
context: Optional context dictionary containing metadata such as:
|
|
116
|
+
- user_id: User identifier for tracking/billing
|
|
117
|
+
- tenant_id: Tenant identifier for multi-tenant setups
|
|
118
|
+
- request_id: Request identifier for tracing
|
|
119
|
+
- session_id: Session identifier
|
|
120
|
+
- Any other custom metadata for observability or middleware
|
|
103
121
|
**kwargs: Additional provider-specific parameters
|
|
104
122
|
|
|
105
123
|
Yields:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image processing utilities for LLM vision support.
|
|
3
|
+
|
|
4
|
+
This module provides functions to handle images in various formats:
|
|
5
|
+
- Image URLs
|
|
6
|
+
- Base64-encoded images
|
|
7
|
+
- Local file paths
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import base64
|
|
12
|
+
import mimetypes
|
|
13
|
+
from typing import Union, Optional, Dict, Any
|
|
14
|
+
from urllib.parse import urlparse
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImageContent:
|
|
21
|
+
"""Represents image content for LLM messages."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
source: str,
|
|
26
|
+
mime_type: Optional[str] = None,
|
|
27
|
+
detail: Optional[str] = None,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize image content.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
source: Image source - can be URL, base64 data URI, or file path
|
|
34
|
+
mime_type: MIME type (e.g., 'image/jpeg', 'image/png'). Auto-detected if not provided.
|
|
35
|
+
detail: Detail level for OpenAI API ('low', 'high', 'auto'). Defaults to 'auto'.
|
|
36
|
+
"""
|
|
37
|
+
self.source = source
|
|
38
|
+
self.mime_type = mime_type or self._detect_mime_type(source)
|
|
39
|
+
self.detail = detail or "auto"
|
|
40
|
+
|
|
41
|
+
def _detect_mime_type(self, source: str) -> str:
|
|
42
|
+
"""Detect MIME type from source."""
|
|
43
|
+
# Check if it's a data URI
|
|
44
|
+
if source.startswith("data:"):
|
|
45
|
+
header = source.split(",")[0]
|
|
46
|
+
mime_type = header.split(":")[1].split(";")[0]
|
|
47
|
+
return mime_type
|
|
48
|
+
|
|
49
|
+
# Check if it's a URL
|
|
50
|
+
if source.startswith(("http://", "https://")):
|
|
51
|
+
parsed = urlparse(source)
|
|
52
|
+
mime_type, _ = mimetypes.guess_type(parsed.path)
|
|
53
|
+
if mime_type and mime_type.startswith("image/"):
|
|
54
|
+
return mime_type
|
|
55
|
+
|
|
56
|
+
# Check if it's a file path
|
|
57
|
+
if os.path.exists(source):
|
|
58
|
+
mime_type, _ = mimetypes.guess_type(source)
|
|
59
|
+
if mime_type and mime_type.startswith("image/"):
|
|
60
|
+
return mime_type
|
|
61
|
+
|
|
62
|
+
# Default to jpeg if cannot detect
|
|
63
|
+
logger.warning(f"Could not detect MIME type for {source}, defaulting to image/jpeg")
|
|
64
|
+
return "image/jpeg"
|
|
65
|
+
|
|
66
|
+
def is_url(self) -> bool:
|
|
67
|
+
"""Check if source is a URL."""
|
|
68
|
+
return self.source.startswith(("http://", "https://"))
|
|
69
|
+
|
|
70
|
+
def is_base64(self) -> bool:
|
|
71
|
+
"""Check if source is a base64 data URI."""
|
|
72
|
+
return self.source.startswith("data:")
|
|
73
|
+
|
|
74
|
+
def is_file_path(self) -> bool:
|
|
75
|
+
"""Check if source is a local file path."""
|
|
76
|
+
return os.path.exists(self.source) and not self.is_url() and not self.is_base64()
|
|
77
|
+
|
|
78
|
+
def get_base64_data(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Get base64-encoded image data.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Base64 string without data URI prefix
|
|
84
|
+
"""
|
|
85
|
+
if self.is_base64():
|
|
86
|
+
# Extract base64 data from data URI
|
|
87
|
+
return self.source.split(",", 1)[1]
|
|
88
|
+
elif self.is_file_path():
|
|
89
|
+
# Read file and encode to base64
|
|
90
|
+
with open(self.source, "rb") as f:
|
|
91
|
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
92
|
+
else:
|
|
93
|
+
raise ValueError(f"Cannot get base64 data from URL: {self.source}. Use URL directly or download first.")
|
|
94
|
+
|
|
95
|
+
def get_url(self) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Get image URL.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
URL string
|
|
101
|
+
"""
|
|
102
|
+
if self.is_url():
|
|
103
|
+
return self.source
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"Source is not a URL: {self.source}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_image_source(source: Union[str, Dict[str, Any]]) -> ImageContent:
|
|
109
|
+
"""
|
|
110
|
+
Parse image source into ImageContent object.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
source: Can be:
|
|
114
|
+
- String URL (http://... or https://...)
|
|
115
|
+
- String base64 data URI (data:image/...;base64,...)
|
|
116
|
+
- String file path
|
|
117
|
+
- Dict with 'url', 'data', or 'path' key
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
ImageContent object
|
|
121
|
+
"""
|
|
122
|
+
if isinstance(source, dict):
|
|
123
|
+
# Handle dict format
|
|
124
|
+
if "url" in source:
|
|
125
|
+
return ImageContent(
|
|
126
|
+
source=source["url"],
|
|
127
|
+
mime_type=source.get("mime_type"),
|
|
128
|
+
detail=source.get("detail"),
|
|
129
|
+
)
|
|
130
|
+
elif "data" in source:
|
|
131
|
+
# Base64 data
|
|
132
|
+
mime_type = source.get("mime_type", "image/jpeg")
|
|
133
|
+
data = source["data"]
|
|
134
|
+
if not data.startswith("data:"):
|
|
135
|
+
data = f"data:{mime_type};base64,{data}"
|
|
136
|
+
return ImageContent(
|
|
137
|
+
source=data,
|
|
138
|
+
mime_type=mime_type,
|
|
139
|
+
detail=source.get("detail"),
|
|
140
|
+
)
|
|
141
|
+
elif "path" in source:
|
|
142
|
+
return ImageContent(
|
|
143
|
+
source=source["path"],
|
|
144
|
+
mime_type=source.get("mime_type"),
|
|
145
|
+
detail=source.get("detail"),
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError(f"Invalid image dict format: {source}")
|
|
149
|
+
elif isinstance(source, str):
|
|
150
|
+
return ImageContent(source=source)
|
|
151
|
+
else:
|
|
152
|
+
raise TypeError(f"Image source must be str or dict, got {type(source)}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def validate_image_source(source: str) -> bool:
|
|
156
|
+
"""
|
|
157
|
+
Validate that image source is accessible.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
source: Image source (URL, base64, or file path)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if source is valid and accessible
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
img = ImageContent(source)
|
|
167
|
+
if img.is_file_path():
|
|
168
|
+
return os.path.exists(source) and os.path.isfile(source)
|
|
169
|
+
elif img.is_url():
|
|
170
|
+
# URL validation - just check format, not accessibility
|
|
171
|
+
parsed = urlparse(source)
|
|
172
|
+
return bool(parsed.scheme and parsed.netloc)
|
|
173
|
+
elif img.is_base64():
|
|
174
|
+
# Base64 validation - check format
|
|
175
|
+
parts = source.split(",", 1)
|
|
176
|
+
return len(parts) == 2 and parts[0].startswith("data:image/")
|
|
177
|
+
return False
|
|
178
|
+
except Exception:
|
|
179
|
+
return False
|
aiecs/main.py
CHANGED
|
@@ -142,7 +142,7 @@ async def lifespan(app: FastAPI):
|
|
|
142
142
|
app = FastAPI(
|
|
143
143
|
title="AIECS - AI Execute Services",
|
|
144
144
|
description="Middleware service for AI-powered task execution and tool orchestration",
|
|
145
|
-
version="1.
|
|
145
|
+
version="1.8.4",
|
|
146
146
|
lifespan=lifespan,
|
|
147
147
|
)
|
|
148
148
|
|
|
@@ -164,7 +164,7 @@ socket_app = socketio.ASGIApp(sio, other_asgi_app=app)
|
|
|
164
164
|
@app.get("/health")
|
|
165
165
|
async def health_check():
|
|
166
166
|
"""Health check endpoint"""
|
|
167
|
-
return {"status": "healthy", "service": "aiecs", "version": "1.
|
|
167
|
+
return {"status": "healthy", "service": "aiecs", "version": "1.8.4"}
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
# Metrics health check endpoint
|
|
@@ -55,6 +55,8 @@ class DocumentFormat(str, Enum):
|
|
|
55
55
|
PLAIN_TEXT = "txt"
|
|
56
56
|
JSON = "json"
|
|
57
57
|
XML = "xml"
|
|
58
|
+
PPTX = "pptx"
|
|
59
|
+
PPT = "ppt"
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
class TemplateType(str, Enum):
|
|
@@ -175,6 +177,9 @@ class DocumentCreatorTool(BaseTool):
|
|
|
175
177
|
# Initialize templates
|
|
176
178
|
self._init_templates()
|
|
177
179
|
|
|
180
|
+
# Initialize office tool for PPTX/DOCX creation
|
|
181
|
+
self._init_office_tool()
|
|
182
|
+
|
|
178
183
|
# Initialize document tracking
|
|
179
184
|
self._documents_created: List[Any] = []
|
|
180
185
|
|
|
@@ -197,6 +202,17 @@ class DocumentCreatorTool(BaseTool):
|
|
|
197
202
|
TemplateType.INVOICE: self._get_invoice_template(),
|
|
198
203
|
}
|
|
199
204
|
|
|
205
|
+
def _init_office_tool(self):
|
|
206
|
+
"""Initialize office tool for PPTX/DOCX creation"""
|
|
207
|
+
try:
|
|
208
|
+
from aiecs.tools.task_tools.office_tool import OfficeTool
|
|
209
|
+
|
|
210
|
+
self.office_tool = OfficeTool()
|
|
211
|
+
self.logger.info("OfficeTool initialized successfully for PPTX/DOCX support")
|
|
212
|
+
except ImportError:
|
|
213
|
+
self.logger.warning("OfficeTool not available, PPTX/DOCX creation will be limited")
|
|
214
|
+
self.office_tool = None
|
|
215
|
+
|
|
200
216
|
# Schema definitions
|
|
201
217
|
class Create_documentSchema(BaseModel):
|
|
202
218
|
"""Schema for create_document operation"""
|
|
@@ -943,7 +959,7 @@ class DocumentCreatorTool(BaseTool):
|
|
|
943
959
|
"questions",
|
|
944
960
|
"contact_info",
|
|
945
961
|
],
|
|
946
|
-
"supported_formats": ["markdown", "html"],
|
|
962
|
+
"supported_formats": ["markdown", "html", "pptx"],
|
|
947
963
|
"style_presets": ["presentation", "modern", "colorful"],
|
|
948
964
|
}
|
|
949
965
|
|
|
@@ -1062,7 +1078,11 @@ class DocumentCreatorTool(BaseTool):
|
|
|
1062
1078
|
) -> str:
|
|
1063
1079
|
"""Generate output path for document"""
|
|
1064
1080
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1065
|
-
|
|
1081
|
+
# Handle PPT format - use pptx extension
|
|
1082
|
+
file_extension = output_format.value
|
|
1083
|
+
if output_format == DocumentFormat.PPT:
|
|
1084
|
+
file_extension = "pptx" # PPT format uses PPTX extension
|
|
1085
|
+
filename = f"{document_type}_{timestamp}_{document_id[:8]}.{file_extension}"
|
|
1066
1086
|
return os.path.join(self.config.output_dir, filename)
|
|
1067
1087
|
|
|
1068
1088
|
def _process_metadata(self, metadata: Dict[str, Any], output_format: DocumentFormat) -> Dict[str, Any]:
|
|
@@ -1175,11 +1195,130 @@ class DocumentCreatorTool(BaseTool):
|
|
|
1175
1195
|
elif output_format == DocumentFormat.JSON:
|
|
1176
1196
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
1177
1197
|
json.dump({"content": content}, f, indent=2, ensure_ascii=False)
|
|
1198
|
+
elif output_format in [DocumentFormat.PPTX, DocumentFormat.PPT]:
|
|
1199
|
+
# Use office_tool to create PPTX file
|
|
1200
|
+
self._write_pptx_file(output_path, content)
|
|
1201
|
+
elif output_format == DocumentFormat.DOCX:
|
|
1202
|
+
# Use office_tool to create DOCX file
|
|
1203
|
+
self._write_docx_file(output_path, content)
|
|
1178
1204
|
else:
|
|
1179
1205
|
# For other formats, write as text for now
|
|
1180
1206
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
1181
1207
|
f.write(content)
|
|
1182
1208
|
|
|
1209
|
+
def _write_pptx_file(self, output_path: str, content: str):
|
|
1210
|
+
"""Write content to PPTX file using office_tool"""
|
|
1211
|
+
if not self.office_tool:
|
|
1212
|
+
raise DocumentCreationError("OfficeTool not available. Cannot create PPTX files.")
|
|
1213
|
+
|
|
1214
|
+
try:
|
|
1215
|
+
# Parse content to extract slides
|
|
1216
|
+
# Slides are separated by "---" or slide markers like "## Slide X:"
|
|
1217
|
+
slides = self._parse_content_to_slides(content)
|
|
1218
|
+
|
|
1219
|
+
# Use office_tool to create PPTX
|
|
1220
|
+
result = self.office_tool.write_pptx(
|
|
1221
|
+
slides=slides,
|
|
1222
|
+
output_path=output_path,
|
|
1223
|
+
image_path=None, # Can be enhanced to extract image paths from metadata
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
if not result.get("success"):
|
|
1227
|
+
raise DocumentCreationError(f"Failed to create PPTX file: {result}")
|
|
1228
|
+
|
|
1229
|
+
self.logger.info(f"PPTX file created successfully: {output_path}")
|
|
1230
|
+
|
|
1231
|
+
except Exception as e:
|
|
1232
|
+
raise DocumentCreationError(f"Failed to write PPTX file: {str(e)}")
|
|
1233
|
+
|
|
1234
|
+
def _write_docx_file(self, output_path: str, content: str):
|
|
1235
|
+
"""Write content to DOCX file using office_tool"""
|
|
1236
|
+
if not self.office_tool:
|
|
1237
|
+
raise DocumentCreationError("OfficeTool not available. Cannot create DOCX files.")
|
|
1238
|
+
|
|
1239
|
+
try:
|
|
1240
|
+
# Use office_tool to create DOCX
|
|
1241
|
+
result = self.office_tool.write_docx(
|
|
1242
|
+
text=content,
|
|
1243
|
+
output_path=output_path,
|
|
1244
|
+
table_data=None, # Can be enhanced to extract tables from content
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
if not result.get("success"):
|
|
1248
|
+
raise DocumentCreationError(f"Failed to create DOCX file: {result}")
|
|
1249
|
+
|
|
1250
|
+
self.logger.info(f"DOCX file created successfully: {output_path}")
|
|
1251
|
+
|
|
1252
|
+
except Exception as e:
|
|
1253
|
+
raise DocumentCreationError(f"Failed to write DOCX file: {str(e)}")
|
|
1254
|
+
|
|
1255
|
+
def _parse_content_to_slides(self, content: str) -> List[str]:
|
|
1256
|
+
"""Parse content string into list of slide contents
|
|
1257
|
+
|
|
1258
|
+
Supports multiple slide separation formats:
|
|
1259
|
+
- "---" separator (markdown style)
|
|
1260
|
+
- "## Slide X:" headers
|
|
1261
|
+
- Empty lines between slides
|
|
1262
|
+
"""
|
|
1263
|
+
slides = []
|
|
1264
|
+
|
|
1265
|
+
# Split by "---" separator (common in markdown presentations)
|
|
1266
|
+
if "---" in content:
|
|
1267
|
+
parts = content.split("---")
|
|
1268
|
+
for part in parts:
|
|
1269
|
+
part = part.strip()
|
|
1270
|
+
if part:
|
|
1271
|
+
# Remove slide headers like "## Slide X: Title"
|
|
1272
|
+
lines = part.split("\n")
|
|
1273
|
+
cleaned_lines = []
|
|
1274
|
+
for line in lines:
|
|
1275
|
+
# Skip slide headers
|
|
1276
|
+
if line.strip().startswith("## Slide") and ":" in line:
|
|
1277
|
+
continue
|
|
1278
|
+
cleaned_lines.append(line)
|
|
1279
|
+
slide_content = "\n".join(cleaned_lines).strip()
|
|
1280
|
+
if slide_content:
|
|
1281
|
+
slides.append(slide_content)
|
|
1282
|
+
else:
|
|
1283
|
+
# Try to split by "## Slide" headers
|
|
1284
|
+
if "## Slide" in content:
|
|
1285
|
+
parts = content.split("## Slide")
|
|
1286
|
+
for i, part in enumerate(parts):
|
|
1287
|
+
if i == 0:
|
|
1288
|
+
# First part might be title slide
|
|
1289
|
+
part = part.strip()
|
|
1290
|
+
if part:
|
|
1291
|
+
slides.append(part)
|
|
1292
|
+
else:
|
|
1293
|
+
# Extract content after "Slide X: Title"
|
|
1294
|
+
lines = part.split("\n", 1)
|
|
1295
|
+
if len(lines) > 1:
|
|
1296
|
+
slide_content = lines[1].strip()
|
|
1297
|
+
if slide_content:
|
|
1298
|
+
slides.append(slide_content)
|
|
1299
|
+
else:
|
|
1300
|
+
# Fallback: split by double newlines (paragraph breaks)
|
|
1301
|
+
parts = content.split("\n\n")
|
|
1302
|
+
current_slide = []
|
|
1303
|
+
for part in parts:
|
|
1304
|
+
part = part.strip()
|
|
1305
|
+
if part:
|
|
1306
|
+
# If it's a header, start a new slide
|
|
1307
|
+
if part.startswith("#"):
|
|
1308
|
+
if current_slide:
|
|
1309
|
+
slides.append("\n".join(current_slide))
|
|
1310
|
+
current_slide = []
|
|
1311
|
+
current_slide.append(part)
|
|
1312
|
+
|
|
1313
|
+
if current_slide:
|
|
1314
|
+
slides.append("\n".join(current_slide))
|
|
1315
|
+
|
|
1316
|
+
# If no slides found, create a single slide with all content
|
|
1317
|
+
if not slides:
|
|
1318
|
+
slides = [content.strip()] if content.strip() else [""]
|
|
1319
|
+
|
|
1320
|
+
return slides
|
|
1321
|
+
|
|
1183
1322
|
def _process_template_variables(self, template_content: str, variables: Dict[str, Any]) -> str:
|
|
1184
1323
|
"""Process template variables in content"""
|
|
1185
1324
|
result = template_content
|
|
@@ -1282,6 +1421,8 @@ class DocumentCreatorTool(BaseTool):
|
|
|
1282
1421
|
".tex": DocumentFormat.LATEX,
|
|
1283
1422
|
".docx": DocumentFormat.DOCX,
|
|
1284
1423
|
".pdf": DocumentFormat.PDF,
|
|
1424
|
+
".pptx": DocumentFormat.PPTX,
|
|
1425
|
+
".ppt": DocumentFormat.PPT,
|
|
1285
1426
|
}
|
|
1286
1427
|
return format_map.get(ext, DocumentFormat.PLAIN_TEXT)
|
|
1287
1428
|
|
|
@@ -798,13 +798,18 @@ class DocumentParserTool(BaseTool):
|
|
|
798
798
|
raise UnsupportedDocumentError("ImageTool not available for image OCR")
|
|
799
799
|
|
|
800
800
|
try:
|
|
801
|
-
# Use image tool for OCR
|
|
802
|
-
|
|
801
|
+
# Use image tool for OCR - the ocr method returns a string directly
|
|
802
|
+
ocr_text = self.image_tool.ocr(file_path=file_path)
|
|
803
803
|
|
|
804
804
|
if strategy == ParsingStrategy.TEXT_ONLY:
|
|
805
|
-
return
|
|
805
|
+
return ocr_text
|
|
806
806
|
else:
|
|
807
|
-
|
|
807
|
+
# Return structured result for other strategies
|
|
808
|
+
return {
|
|
809
|
+
"text": ocr_text,
|
|
810
|
+
"file_path": file_path,
|
|
811
|
+
"document_type": DocumentType.IMAGE,
|
|
812
|
+
}
|
|
808
813
|
|
|
809
814
|
except Exception as e:
|
|
810
815
|
raise ParseError(f"Failed to parse image document: {str(e)}")
|