quantalogic 0.2.22__py3-none-any.whl → 0.2.23__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.
quantalogic/agent.py CHANGED
@@ -483,7 +483,7 @@ class Agent(BaseModel):
483
483
  if len(response) > MAX_RESPONSE_LENGTH:
484
484
  response_display = response[:MAX_RESPONSE_LENGTH]
485
485
  response_display += (
486
- f"... content was trunctated full content available by interpolation in variable {variable_name}"
486
+ f"... content was truncated full content available by interpolation in variable {variable_name}"
487
487
  )
488
488
 
489
489
  # Format the response message
@@ -3,8 +3,9 @@
3
3
  # Standard library imports
4
4
 
5
5
  # Local application imports
6
+ from dotenv import load_dotenv
7
+
6
8
  from quantalogic.agent import Agent
7
- from quantalogic.coding_agent import create_coding_agent
8
9
  from quantalogic.console_print_token import console_print_token
9
10
  from quantalogic.tools import (
10
11
  AgentTool,
@@ -14,6 +15,7 @@ from quantalogic.tools import (
14
15
  ExecuteBashCommandTool,
15
16
  InputQuestionTool,
16
17
  ListDirectoryTool,
18
+ LLMImageGenerationTool,
17
19
  LLMTool,
18
20
  LLMVisionTool,
19
21
  MarkitdownTool,
@@ -21,6 +23,7 @@ from quantalogic.tools import (
21
23
  PythonTool,
22
24
  ReadFileBlockTool,
23
25
  ReadFileTool,
26
+ ReadHTMLTool,
24
27
  ReplaceInFileTool,
25
28
  RipgrepTool,
26
29
  SearchDefinitionNames,
@@ -29,6 +32,8 @@ from quantalogic.tools import (
29
32
  WriteFileTool,
30
33
  )
31
34
 
35
+ load_dotenv()
36
+
32
37
  MODEL_NAME = "deepseek/deepseek-chat"
33
38
 
34
39
 
@@ -66,6 +71,12 @@ def create_agent(
66
71
  MarkitdownTool(),
67
72
  LLMTool(model_name=model_name, on_token=console_print_token if not no_stream else None),
68
73
  DownloadHttpFileTool(),
74
+ LLMImageGenerationTool(
75
+ provider="dall-e",
76
+ model_name="openai/dall-e-3",
77
+ on_token=console_print_token if not no_stream else None
78
+ ),
79
+ ReadHTMLTool()
69
80
  ]
70
81
 
71
82
  if vision_model_name:
@@ -115,6 +126,7 @@ def create_interpreter_agent(
115
126
  MarkitdownTool(),
116
127
  LLMTool(model_name=model_name, on_token=console_print_token if not no_stream else None),
117
128
  DownloadHttpFileTool(),
129
+ ReadHTMLTool(),
118
130
  ]
119
131
  return Agent(
120
132
  model_name=model_name,
@@ -163,6 +175,7 @@ def create_full_agent(
163
175
  DownloadHttpFileTool(),
164
176
  WikipediaSearchTool(),
165
177
  DuckDuckGoSearchTool(),
178
+ ReadHTMLTool(),
166
179
  ]
167
180
 
168
181
  if vision_model_name:
@@ -212,6 +225,7 @@ def create_basic_agent(
212
225
  InputQuestionTool(),
213
226
  ExecuteBashCommandTool(),
214
227
  LLMTool(model_name=model_name, on_token=console_print_token if not no_stream else None),
228
+ ReadHTMLTool(),
215
229
  ]
216
230
 
217
231
  if vision_model_name:
@@ -11,6 +11,7 @@ from quantalogic.tools import (
11
11
  LLMVisionTool,
12
12
  ReadFileBlockTool,
13
13
  ReadFileTool,
14
+ ReadHTMLTool,
14
15
  ReplaceInFileTool,
15
16
  RipgrepTool,
16
17
  SearchDefinitionNames,
@@ -74,6 +75,7 @@ def create_coding_agent(
74
75
  InputQuestionTool(),
75
76
  DuckDuckGoSearchTool(),
76
77
  JinjaTool(),
78
+ ReadHTMLTool()
77
79
  ]
78
80
 
79
81
  if vision_model_name:
@@ -1,10 +1,12 @@
1
1
  """Generative model module for AI-powered text generation."""
2
2
 
3
3
  import functools
4
+ from typing import Dict, Any, Optional, List
5
+ from datetime import datetime
4
6
 
5
7
  import litellm
6
8
  import openai
7
- from litellm import completion, exceptions, get_max_tokens, get_model_info, token_counter
9
+ from litellm import completion, exceptions, get_max_tokens, get_model_info, token_counter, image_generation
8
10
  from loguru import logger
9
11
  from pydantic import BaseModel, Field, field_validator
10
12
 
@@ -70,10 +72,12 @@ class ResponseStats(BaseModel):
70
72
  usage: TokenUsage
71
73
  model: str
72
74
  finish_reason: str | None = None
75
+ data: List[Dict[str, Any]] | None = None
76
+ created: str | None = None
73
77
 
74
78
 
75
79
  class GenerativeModel:
76
- """Generative model for AI-powered text generation with configurable parameters."""
80
+ """Generative model for AI-powered text generation and image generation."""
77
81
 
78
82
  def __init__(
79
83
  self,
@@ -311,3 +315,73 @@ class GenerativeModel:
311
315
  except Exception as e:
312
316
  logger.error(f"Error getting max output tokens for {self.model}: {e}")
313
317
  return None
318
+
319
+ def generate_image(self, prompt: str, params: Dict[str, Any]) -> ResponseStats:
320
+ """Generate an image using the specified model and parameters.
321
+
322
+ Args:
323
+ prompt: Text description of the image to generate
324
+ params: Dictionary of parameters for image generation including:
325
+ - model: Name of the image generation model
326
+ - size: Size of the generated image
327
+ - quality: Quality level (DALL-E only)
328
+ - style: Style preference (DALL-E only)
329
+ - response_format: Format of the response (url/base64)
330
+ - negative_prompt: What to avoid in the image (SD only)
331
+ - cfg_scale: Classifier Free Guidance scale (SD only)
332
+
333
+ Returns:
334
+ ResponseStats containing the image generation results
335
+
336
+ Raises:
337
+ Exception: If there's an error during image generation
338
+ """
339
+ try:
340
+ logger.debug(f"Generating image with params: {params}")
341
+
342
+ # Ensure prompt is in params
343
+ generation_params = {**params}
344
+ generation_params["prompt"] = prompt
345
+
346
+ # Call litellm's image generation function
347
+ response = image_generation(
348
+ model=generation_params.pop("model"),
349
+ **generation_params
350
+ )
351
+
352
+ # Convert response data to list of dictionaries with string values
353
+ if hasattr(response, "data"):
354
+ data = []
355
+ for img in response.data:
356
+ img_data = {}
357
+ if hasattr(img, "url"):
358
+ img_data["url"] = str(img.url)
359
+ if hasattr(img, "b64_json"):
360
+ img_data["b64_json"] = str(img.b64_json)
361
+ if hasattr(img, "revised_prompt"):
362
+ img_data["revised_prompt"] = str(img.revised_prompt)
363
+ data.append(img_data)
364
+ else:
365
+ data = [{"url": str(response.url)}]
366
+
367
+ # Convert timestamp to ISO format string
368
+ if hasattr(response, "created"):
369
+ try:
370
+ created = datetime.fromtimestamp(response.created).isoformat()
371
+ except (TypeError, ValueError):
372
+ created = str(response.created)
373
+ else:
374
+ created = None
375
+
376
+ # Convert response to our ResponseStats format
377
+ return ResponseStats(
378
+ response="", # Empty for image generation
379
+ usage=TokenUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
380
+ model=str(params["model"]),
381
+ data=data,
382
+ created=created
383
+ )
384
+
385
+ except Exception as e:
386
+ logger.error(f"Error in image generation: {str(e)}")
387
+ raise
quantalogic/main.py CHANGED
@@ -28,11 +28,11 @@ from quantalogic.agent import Agent # noqa: E402
28
28
  # Local application imports
29
29
  from quantalogic.agent_config import ( # noqa: E402
30
30
  MODEL_NAME,
31
- create_coding_agent,
31
+ create_basic_agent,
32
32
  create_full_agent,
33
33
  create_interpreter_agent,
34
- create_basic_agent,
35
34
  )
35
+ from quantalogic.coding_agent import create_coding_agent # noqa: E402
36
36
  from quantalogic.interactive_text_editor import get_multiline_input # noqa: E402
37
37
  from quantalogic.search_agent import create_search_agent # noqa: E402
38
38
 
@@ -1,6 +1,7 @@
1
1
  """Tools for the QuantaLogic agent."""
2
2
 
3
3
  from .agent_tool import AgentTool
4
+ from .dalle_e import LLMImageGenerationTool
4
5
  from .download_http_file_tool import DownloadHttpFileTool
5
6
  from .duckduckgo_search_tool import DuckDuckGoSearchTool
6
7
  from .edit_whole_content_tool import EditWholeContentTool
@@ -16,6 +17,7 @@ from .nodejs_tool import NodeJsTool
16
17
  from .python_tool import PythonTool
17
18
  from .read_file_block_tool import ReadFileBlockTool
18
19
  from .read_file_tool import ReadFileTool
20
+ from .read_html_tool import ReadHTMLTool
19
21
  from .replace_in_file_tool import ReplaceInFileTool
20
22
  from .ripgrep_tool import RipgrepTool
21
23
  from .search_definition_names import SearchDefinitionNames
@@ -53,4 +55,6 @@ __all__ = [
53
55
  "DownloadHttpFileTool",
54
56
  "EditWholeContentTool",
55
57
  "JinjaTool",
58
+ "LLMImageGenerationTool",
59
+ "ReadHTMLTool"
56
60
  ]
@@ -0,0 +1,270 @@
1
+ """LLM Image Generation Tool for creating images using DALL-E or Stable Diffusion via AWS Bedrock."""
2
+
3
+ import datetime
4
+ import json
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional
8
+
9
+ import requests
10
+ from loguru import logger
11
+ from pydantic import ConfigDict, Field
12
+ from tenacity import retry, stop_after_attempt, wait_exponential
13
+
14
+ from quantalogic.generative_model import GenerativeModel
15
+ from quantalogic.tools.tool import Tool, ToolArgument
16
+
17
+
18
+ class ImageProvider(str, Enum):
19
+ """Supported image generation providers."""
20
+ DALLE = "dall-e"
21
+ STABLE_DIFFUSION = "stable-diffusion"
22
+
23
+ PROVIDER_CONFIGS = {
24
+ ImageProvider.DALLE: {
25
+ "model_name": "dall-e-3",
26
+ "sizes": ["1024x1024", "1024x1792", "1792x1024"],
27
+ "qualities": ["standard", "hd"],
28
+ "styles": ["vivid", "natural"],
29
+ },
30
+ ImageProvider.STABLE_DIFFUSION: {
31
+ #"model_name": "anthropic.claude-3-sonnet-20240229",
32
+ "model_name": "amazon.titan-image-generator-v1",
33
+ "sizes": ["1024x1024"], # Bedrock SD supported size
34
+ "qualities": ["standard"], # SD quality is controlled by cfg_scale
35
+ "styles": ["none"], # Style is controlled through prompting
36
+ }
37
+ }
38
+
39
+ class LLMImageGenerationTool(Tool):
40
+ """Tool for generating images using DALL-E or Stable Diffusion."""
41
+
42
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
43
+
44
+ name: str = Field(default="llm_image_generation_tool")
45
+ description: str = Field(
46
+ default=(
47
+ "Generate images using DALL-E or Stable Diffusion. "
48
+ "Supports different sizes, styles, and quality settings."
49
+ )
50
+ )
51
+ arguments: list = Field(
52
+ default=[
53
+ ToolArgument(
54
+ name="prompt",
55
+ arg_type="string",
56
+ description="Text description of the image to generate",
57
+ required=True,
58
+ example="A serene Japanese garden with a red maple tree",
59
+ ),
60
+ ToolArgument(
61
+ name="provider",
62
+ arg_type="string",
63
+ description="Image generation provider (dall-e or stable-diffusion)",
64
+ required=False,
65
+ default="dall-e",
66
+ example="dall-e",
67
+ ),
68
+ ToolArgument(
69
+ name="size",
70
+ arg_type="string",
71
+ description="Size of the generated image",
72
+ required=False,
73
+ default="1024x1024",
74
+ example="1024x1024",
75
+ ),
76
+ ToolArgument(
77
+ name="quality",
78
+ arg_type="string",
79
+ description="Quality level for DALL-E",
80
+ required=False,
81
+ default="standard",
82
+ example="standard",
83
+ ),
84
+ ToolArgument(
85
+ name="style",
86
+ arg_type="string",
87
+ description="Style preference for DALL-E",
88
+ required=False,
89
+ default="vivid",
90
+ example="vivid",
91
+ ),
92
+ ToolArgument(
93
+ name="negative_prompt",
94
+ arg_type="string",
95
+ description="What to avoid in the image (Stable Diffusion only)",
96
+ required=False,
97
+ default="",
98
+ example="blurry, low quality",
99
+ ),
100
+ ToolArgument(
101
+ name="cfg_scale",
102
+ arg_type="string",
103
+ description="Classifier Free Guidance scale (Stable Diffusion only)",
104
+ required=False,
105
+ default="7.5",
106
+ example="7.5",
107
+ ),
108
+ ]
109
+ )
110
+
111
+ provider: ImageProvider = Field(default=ImageProvider.DALLE)
112
+ model_name: str = Field(default=PROVIDER_CONFIGS[ImageProvider.DALLE]["model_name"])
113
+ output_dir: Path = Field(default=Path("generated_images"))
114
+ generative_model: Optional[GenerativeModel] = Field(default=None)
115
+
116
+ def model_post_init(self, __context):
117
+ """Initialize after model creation."""
118
+ if self.generative_model is None:
119
+ self.generative_model = GenerativeModel(model=self.model_name)
120
+ logger.debug(f"Initialized LLMImageGenerationTool with model: {self.model_name}")
121
+
122
+ # Create output directory if it doesn't exist
123
+ self.output_dir.mkdir(parents=True, exist_ok=True)
124
+
125
+ def _validate_dalle_params(self, size: str, quality: str, style: str) -> None:
126
+ """Validate DALL-E specific parameters."""
127
+ if size not in PROVIDER_CONFIGS[ImageProvider.DALLE]["sizes"]:
128
+ raise ValueError(f"Invalid size for DALL-E. Must be one of: {PROVIDER_CONFIGS[ImageProvider.DALLE]['sizes']}")
129
+ if quality not in PROVIDER_CONFIGS[ImageProvider.DALLE]["qualities"]:
130
+ raise ValueError(f"Invalid quality for DALL-E. Must be one of: {PROVIDER_CONFIGS[ImageProvider.DALLE]['qualities']}")
131
+ if style not in PROVIDER_CONFIGS[ImageProvider.DALLE]["styles"]:
132
+ raise ValueError(f"Invalid style for DALL-E. Must be one of: {PROVIDER_CONFIGS[ImageProvider.DALLE]['styles']}")
133
+
134
+ def _validate_sd_params(self, size: str, cfg_scale: float) -> None:
135
+ """Validate Stable Diffusion specific parameters."""
136
+ if size not in PROVIDER_CONFIGS[ImageProvider.STABLE_DIFFUSION]["sizes"]:
137
+ raise ValueError(f"Invalid size for Stable Diffusion. Must be one of: {PROVIDER_CONFIGS[ImageProvider.STABLE_DIFFUSION]['sizes']}")
138
+ if not 1.0 <= cfg_scale <= 20.0:
139
+ raise ValueError("cfg_scale must be between 1.0 and 20.0")
140
+
141
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
142
+ def _save_image(self, image_url: str, filename: str) -> Path:
143
+ """Download and save image locally with retry logic."""
144
+ try:
145
+ response = requests.get(image_url, timeout=30)
146
+ response.raise_for_status()
147
+
148
+ file_path = self.output_dir / filename
149
+ file_path.write_bytes(response.content)
150
+ logger.info(f"Image saved successfully at: {file_path}")
151
+ return file_path
152
+
153
+ except Exception as e:
154
+ logger.error(f"Error saving image: {e}")
155
+ raise
156
+
157
+ def _save_metadata(self, metadata: Dict[str, Any]) -> None:
158
+ """Save image metadata to JSON file."""
159
+ try:
160
+ metadata_path = self.output_dir / f"{metadata['filename']}.json"
161
+ with open(metadata_path, 'w') as f:
162
+ json.dump(metadata, f, indent=2)
163
+ logger.info(f"Metadata saved successfully at: {metadata_path}")
164
+ except Exception as e:
165
+ logger.error(f"Error saving metadata: {e}")
166
+ raise
167
+
168
+ def execute(
169
+ self,
170
+ prompt: str,
171
+ provider: str = "dall-e",
172
+ size: str = "1024x1024",
173
+ quality: str = "standard",
174
+ style: str = "vivid",
175
+ negative_prompt: str = "",
176
+ cfg_scale: str = "7.5",
177
+ ) -> str:
178
+ """Execute the tool to generate an image based on the prompt.
179
+
180
+ Args:
181
+ prompt: Text description of the image to generate
182
+ provider: Provider to use (dall-e or stable-diffusion)
183
+ size: Size of the generated image
184
+ quality: Quality level for DALL-E
185
+ style: Style preference for DALL-E
186
+ negative_prompt: What to avoid in the image (Stable Diffusion only)
187
+ cfg_scale: Classifier Free Guidance scale (Stable Diffusion only)
188
+
189
+ Returns:
190
+ Path to the locally saved image
191
+ """
192
+ try:
193
+ provider_enum = ImageProvider(provider.lower())
194
+
195
+ # Convert cfg_scale to float only if it's not empty and we're using Stable Diffusion
196
+ cfg_scale_float = float(cfg_scale) if cfg_scale and provider_enum == ImageProvider.STABLE_DIFFUSION else None
197
+
198
+ # Validate parameters based on provider
199
+ if provider_enum == ImageProvider.DALLE:
200
+ self._validate_dalle_params(size, quality, style)
201
+ params = {
202
+ "model": PROVIDER_CONFIGS[provider_enum]["model_name"],
203
+ "size": size,
204
+ "quality": quality,
205
+ "style": style,
206
+ "response_format": "url"
207
+ }
208
+ else: # Stable Diffusion
209
+ if cfg_scale_float is None:
210
+ cfg_scale_float = 7.5 # Default value
211
+ self._validate_sd_params(size, cfg_scale_float)
212
+ params = {
213
+ "model": PROVIDER_CONFIGS[provider_enum]["model_name"],
214
+ "negative_prompt": negative_prompt,
215
+ "cfg_scale": cfg_scale_float,
216
+ "size": size,
217
+ "response_format": "url"
218
+ }
219
+
220
+ # Generate image
221
+ logger.info(f"Generating image with {provider} using params: {params}")
222
+ response = self.generative_model.generate_image(
223
+ prompt=prompt,
224
+ params=params
225
+ )
226
+
227
+ # Extract image data from response
228
+ if not response.data:
229
+ raise ValueError("No image data in response")
230
+
231
+ image_data = response.data[0] # First image from the response
232
+ image_url = str(image_data.get("url", ""))
233
+ revised_prompt = str(image_data.get("revised_prompt", prompt))
234
+
235
+ if not image_url:
236
+ raise ValueError("No image URL in response")
237
+
238
+ # Save image locally
239
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
240
+ filename = f"{provider}_{timestamp}.png"
241
+ local_path = self._save_image(image_url, filename)
242
+
243
+ # Save metadata
244
+ metadata = {
245
+ "filename": str(filename),
246
+ "prompt": str(prompt),
247
+ "revised_prompt": str(revised_prompt),
248
+ "provider": str(provider),
249
+ "model": str(response.model),
250
+ "created": str(response.created or ""),
251
+ "parameters": {k: str(v) for k, v in {**params, "prompt": prompt}.items()},
252
+ "image_url": str(image_url),
253
+ "local_path": str(local_path)
254
+ }
255
+ self._save_metadata(metadata)
256
+
257
+ logger.info(f"Image generated and saved at: {local_path}")
258
+ return str(local_path)
259
+
260
+ except Exception as e:
261
+ logger.error(f"Error generating image with {provider}: {e}")
262
+ raise Exception(f"Error generating image with {provider}: {e}") from e
263
+
264
+
265
+ if __name__ == "__main__":
266
+ # Example usage
267
+ tool = LLMImageGenerationTool()
268
+ prompt = "A serene Japanese garden with a red maple tree"
269
+ image_path = tool.execute(prompt=prompt)
270
+ print(f"Image saved at: {image_path}")
@@ -14,7 +14,7 @@ class ReadFileTool(Tool):
14
14
 
15
15
  name: str = "read_file_tool"
16
16
  description: str = (
17
- f"Reads a local file or HTTP content and returns its content."
17
+ f"Reads a local file content and returns its content."
18
18
  f"Cut to {MAX_LINES} first lines.\n"
19
19
  "Don't use on HTML files and large files."
20
20
  "Prefer to use read file block tool to don't fill the memory."
@@ -23,9 +23,9 @@ class ReadFileTool(Tool):
23
23
  ToolArgument(
24
24
  name="file_path",
25
25
  arg_type="string",
26
- description="The path to the file or URL to read.",
26
+ description="The path to the file to read.",
27
27
  required=True,
28
- example="/path/to/file.txt or https://example.com/data.txt",
28
+ example="/path/to/file.txt",
29
29
  ),
30
30
  ]
31
31
 
@@ -0,0 +1,303 @@
1
+ import os
2
+ import random
3
+ import time
4
+ from typing import Optional
5
+
6
+ import requests
7
+ from bs4 import BeautifulSoup
8
+ from loguru import logger
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+ # Ensure that markdownify is installed: pip install markdownify
12
+ try:
13
+ from markdownify import markdownify as md
14
+ except ImportError:
15
+ logger.error("Missing dependency: markdownify. Install it using 'pip install markdownify'")
16
+ raise
17
+
18
+ # Assuming Tool and ToolArgument are properly defined in quantalogic.tools.tool
19
+ from quantalogic.tools.tool import Tool, ToolArgument
20
+
21
+ # User-Agent list to mimic different browsers
22
+ USER_AGENTS = [
23
+ # Chrome on Windows
24
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
25
+ " Chrome/91.0.4472.124 Safari/537.36",
26
+ # Chrome on macOS
27
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"
28
+ " Chrome/91.0.4472.124 Safari/537.36",
29
+ # Firefox on Windows
30
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
31
+ # Firefox on macOS
32
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0",
33
+ # Safari on macOS
34
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)"
35
+ " Version/14.1.1 Safari/605.1.15"
36
+ ]
37
+
38
+ # Additional headers to mimic real browser requests
39
+ ADDITIONAL_HEADERS = {
40
+ "Accept": "text/html,application/xhtml+xml,application/xml;"
41
+ "q=0.9,image/webp,*/*;q=0.8",
42
+ "Accept-Language": "en-US,en;q=0.5",
43
+ "Upgrade-Insecure-Requests": "1",
44
+ "DNT": "1", # Do Not Track
45
+ "Connection": "keep-alive",
46
+ "Cache-Control": "max-age=0"
47
+ }
48
+
49
+ class ReadHTMLTool(Tool):
50
+ """Tool for reading HTML content from files or URLs in specified line ranges."""
51
+
52
+ class Arguments(BaseModel):
53
+ source: str = Field(
54
+ ...,
55
+ description="The file path or URL to read HTML from",
56
+ example="https://example.com or ./example.html"
57
+ )
58
+ convert: Optional[str] = Field(
59
+ "text",
60
+ description="Convert input to 'text' (Markdown) or 'html' no conversion. Default is 'text'",
61
+ example="'text' or 'html'"
62
+ )
63
+ line_start: Optional[int] = Field(
64
+ 1,
65
+ description="The starting line number (1-based index). Default: 1",
66
+ ge=1,
67
+ example="1"
68
+ )
69
+ line_end: Optional[int] = Field(
70
+ 300,
71
+ description="The ending line number (1-based index). Default: 300",
72
+ ge=1,
73
+ example="300"
74
+ )
75
+
76
+ @field_validator('convert')
77
+ def validate_convert(cls, v):
78
+ if v not in ["text", "html"]:
79
+ raise ValueError("Convert must be either 'text' or 'html'")
80
+ return v
81
+
82
+ @field_validator('line_end')
83
+ def validate_line_end(cls, v, values):
84
+ if 'line_start' in values and v < values['line_start']:
85
+ raise ValueError("line_end must be greater than or equal to line_start")
86
+ return v
87
+
88
+ name: str = "read_html_tool"
89
+ description: str = (
90
+ "Reads HTML content from either a file path or URL in specified line ranges. "
91
+ "Returns parsed HTML content using BeautifulSoup or converts it to Markdown. "
92
+ "Allows reading specific portions of HTML files by defining start and end lines."
93
+ )
94
+ arguments: list = [
95
+ ToolArgument(
96
+ name="source",
97
+ arg_type="string",
98
+ description="The file path or URL to read HTML from",
99
+ required=True,
100
+ example="https://example.com or ./example.html"
101
+ ),
102
+ ToolArgument(
103
+ name="convert",
104
+ arg_type="string",
105
+ description="Convert input to 'text' (Markdown) or 'html'. Default is 'text'",
106
+ default='text',
107
+ required=False,
108
+ example="'text' or 'html'"
109
+ ),
110
+ ToolArgument(
111
+ name="line_start",
112
+ arg_type="int",
113
+ description="The starting line number (1-based index). Default: 1",
114
+ required=False,
115
+ example="1"
116
+ ),
117
+ ToolArgument(
118
+ name="line_end",
119
+ arg_type="int",
120
+ description="The ending line number (1-based index). Default: 300",
121
+ required=False,
122
+ example="300"
123
+ )
124
+ ]
125
+
126
+ def validate_source(self, source: str) -> bool:
127
+ """Validate if source is a valid file path or URL."""
128
+ if os.path.isfile(source):
129
+ return True
130
+ try:
131
+ result = requests.utils.urlparse(source)
132
+ return all([result.scheme, result.netloc])
133
+ except (requests.exceptions.RequestException, ValueError) as e:
134
+ # Log the specific exception for debugging
135
+ logger.debug(f"URL validation failed: {e}")
136
+ return False
137
+
138
+ def read_from_file(self, file_path: str) -> str:
139
+ """Read HTML content from a file."""
140
+ try:
141
+ with open(file_path, encoding='utf-8') as file:
142
+ return file.read()
143
+ except Exception as e:
144
+ logger.error(f"Error reading file: {e}")
145
+ raise ValueError(f"Error reading file: {e}")
146
+
147
+ def read_from_url(self, url: str) -> str:
148
+ """Read HTML content from a URL with randomized User-Agent and headers."""
149
+ try:
150
+ # Randomize User-Agent
151
+ headers = ADDITIONAL_HEADERS.copy()
152
+ headers["User-Agent"] = random.choice(USER_AGENTS)
153
+
154
+ # Add a small random delay to mimic human behavior
155
+ time.sleep(random.uniform(0.5, 2.0))
156
+
157
+ # Use a timeout to prevent hanging
158
+ response = requests.get(
159
+ url,
160
+ headers=headers,
161
+ timeout=10,
162
+ allow_redirects=True
163
+ )
164
+ response.raise_for_status()
165
+ return response.text
166
+ except requests.RequestException as e:
167
+ logger.error(f"Error fetching URL {url}: {e}")
168
+ raise ValueError(f"Error fetching URL: {e}")
169
+
170
+ def parse_html(self, html_content: str) -> BeautifulSoup:
171
+ """Parse HTML content using BeautifulSoup."""
172
+ try:
173
+ return BeautifulSoup(html_content, 'html.parser')
174
+ except Exception as e:
175
+ logger.error(f"Error parsing HTML: {e}")
176
+ raise ValueError(f"Error parsing HTML: {e}")
177
+
178
+ def read_source(self, source: str) -> str:
179
+ """Read entire content from source."""
180
+ if os.path.isfile(source):
181
+ return self.read_from_file(source)
182
+ else:
183
+ return self.read_from_url(source)
184
+
185
+ def _convert_content(self, content: str, convert_type: str) -> str:
186
+ """
187
+ Convert content based on the specified type.
188
+
189
+ Args:
190
+ content (str): The input content to convert
191
+ convert_type (str): The type of conversion to perform
192
+
193
+ Returns:
194
+ str: Converted content
195
+ """
196
+ if convert_type == "text":
197
+ # Convert HTML to Markdown using markdownify
198
+ try:
199
+ markdown_content = md(content, heading_style="ATX")
200
+ return markdown_content
201
+ except Exception as e:
202
+ logger.error(f"Error converting HTML to Markdown: {e}")
203
+ raise ValueError(f"Error converting HTML to Markdown: {e}")
204
+
205
+ if convert_type == "html":
206
+ # Ensure content is valid HTML
207
+ try:
208
+ soup = BeautifulSoup(content, 'html.parser')
209
+ return soup.prettify()
210
+ except Exception as e:
211
+ logger.error(f"Error prettifying HTML: {e}")
212
+ raise ValueError(f"Error prettifying HTML: {e}")
213
+
214
+ return content
215
+
216
+ def execute(self, source: str, convert: Optional[str] = 'text',
217
+ line_start: int = 1, line_end: int = 300) -> str:
218
+ """Execute the tool to read and parse HTML content in specified line ranges."""
219
+ logger.debug(f"Executing read_html_tool with source: {source}")
220
+
221
+ line_start = int(line_start)
222
+ line_end = int(line_end)
223
+
224
+ if not self.validate_source(source):
225
+ logger.warning(f"Invalid source: {source}")
226
+ return f"Invalid source: {source}"
227
+
228
+ try:
229
+ # Step 1: Read entire content from source
230
+ raw_content = self.read_source(source)
231
+
232
+ # Step 2: Convert content
233
+ converted_content = self._convert_content(raw_content, convert)
234
+
235
+ # Step 3: Split converted content into lines
236
+ lines = converted_content.splitlines()
237
+ total_lines = len(lines)
238
+
239
+ # Step 4: Adjust line_end if it exceeds total_lines
240
+ adjusted_end_line = min(line_end, total_lines)
241
+
242
+ # Step 5: Slice lines based on line_start and adjusted_end_line
243
+ sliced_lines = lines[line_start - 1: adjusted_end_line]
244
+ sliced_content = "\n".join(sliced_lines)
245
+
246
+ # Step 6: Calculate actual_end_line based on lines returned
247
+ if sliced_lines:
248
+ actual_end_line = line_start + len(sliced_lines) - 1
249
+ else:
250
+ actual_end_line = line_start
251
+
252
+ # Step 7: Determine if this is the last block
253
+ is_last_block = actual_end_line >= total_lines
254
+
255
+ # Step 8: Calculate total lines returned
256
+ total_lines_returned = len(sliced_lines)
257
+
258
+ # Prepare detailed output
259
+ result = [
260
+ f"==== Source: {source} ====",
261
+ f"==== Lines: {line_start} - {actual_end_line} of {total_lines} ====",
262
+ "==== Block Detail ====",
263
+ f"Start Line: {line_start}",
264
+ f"End Line: {actual_end_line}",
265
+ f"Total Lines Returned: {total_lines_returned}",
266
+ f"Is Last Block: {'Yes' if is_last_block else 'No'}",
267
+ "==== Content ====",
268
+ sliced_content,
269
+ "==== End of Block ===="
270
+ ]
271
+
272
+ return "\n".join(result)
273
+
274
+ except Exception as e:
275
+ logger.error(f"Unexpected error processing source {source}: {e}")
276
+ return f"Unexpected error: {e}"
277
+
278
+
279
+ if __name__ == "__main__":
280
+ tool = ReadHTMLTool()
281
+
282
+ # Since to_markdown() is not defined, we'll comment it out.
283
+ # print(tool.to_markdown())
284
+
285
+ # Test with a known working URL
286
+ try:
287
+ result = tool.execute(source="https://www.quantalogic.app", line_start=1, line_end=100)
288
+ print("URL Test Result:")
289
+ print(result)
290
+ except Exception as e:
291
+ print(f"URL Test Failed: {e}")
292
+
293
+ # Test with local file (if available)
294
+ try:
295
+ local_file = os.path.join(os.path.dirname(__file__), "test.html")
296
+ if os.path.exists(local_file):
297
+ result = tool.execute(source=local_file, line_start=1, line_end=1000)
298
+ print("Local File Test Result:")
299
+ print(result)
300
+ else:
301
+ print("No local test file found.")
302
+ except Exception as e:
303
+ print(f"Local File Test Failed: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: quantalogic
3
- Version: 0.2.22
3
+ Version: 0.2.23
4
4
  Summary: QuantaLogic ReAct Agents
5
5
  Author: Raphaël MANSUY
6
6
  Author-email: raphael.mansuy@gmail.com
@@ -8,6 +8,7 @@ Requires-Python: >=3.12,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: beautifulsoup4 (>=4.12.3,<5.0.0)
11
12
  Requires-Dist: boto3 (>=1.35.86,<2.0.0)
12
13
  Requires-Dist: click (>=8.1.8,<9.0.0)
13
14
  Requires-Dist: duckduckgo-search (>=7.2.1,<8.0.0)
@@ -18,6 +19,7 @@ Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
18
19
  Requires-Dist: litellm (>=1.56.4,<2.0.0)
19
20
  Requires-Dist: llmlingua (>=0.2.2,<0.3.0)
20
21
  Requires-Dist: loguru (>=0.7.3,<0.8.0)
22
+ Requires-Dist: markdownify (>=0.14.1,<0.15.0)
21
23
  Requires-Dist: markitdown (>=0.0.1a3,<0.0.2)
22
24
  Requires-Dist: mkdocs-git-revision-date-localized-plugin (>=1.2.0,<2.0.0)
23
25
  Requires-Dist: mkdocs-macros-plugin (>=1.0.4,<2.0.0)
@@ -66,6 +68,7 @@ The `cli` version include coding capabilities comparable to Aider.
66
68
 
67
69
  ![Video](./examples/generated_tutorials/python/quantalogic_8s.gif)
68
70
 
71
+
69
72
  [HowTo Guide](./docs/howto/howto.md)
70
73
 
71
74
  ## Why QuantaLogic?
@@ -1,14 +1,14 @@
1
1
  quantalogic/__init__.py,sha256=kX0c_xmD9OslWnAE92YHMGuD7xZcTo8ZOF_5R64HKps,784
2
- quantalogic/agent.py,sha256=yYRfp5NM9GhWWiAJnsLbgq-Yf9ikGGsEGwFL3MbUHXA,31227
3
- quantalogic/agent_config.py,sha256=OA6GCUPYUWYYPvIjPsnrpbDm4GzXM8BzkFm39rn6EcU,7241
4
- quantalogic/coding_agent.py,sha256=cFw-D6yLNsQpotj8z5-kLaudUMjHJf3XW3ZOyScZTSc,4894
2
+ quantalogic/agent.py,sha256=4Z3ImHbLs3wThspoSYR-mR_zSExMf1nwcLGxZjTQLXY,31226
3
+ quantalogic/agent_config.py,sha256=9sjDnCPlAqVM45oguB_D509WSCaXZmuaVUtLcOvDlPg,7572
4
+ quantalogic/coding_agent.py,sha256=UJ0fdKKA8XSB2py0NW4-e-orozo78ZAprXWuortYBiA,4935
5
5
  quantalogic/console_print_events.py,sha256=KB-DGi52As8M96eUs1N_vgNqKIFtqv_H8NTOd3TLTgQ,2163
6
6
  quantalogic/console_print_token.py,sha256=qSU-3kmoZk4T5-1ybrEBi8tIXDPcz7eyWKhGh3E8uIg,395
7
7
  quantalogic/docs_cli.py,sha256=3giVbUpespB9ZdTSJ955A3BhcOaBl5Lwsn1AVy9XAeY,1663
8
8
  quantalogic/event_emitter.py,sha256=jqot2g4JRXc88K6PW837Oqxbf7shZfO-xdPaUWmzupk,7901
9
- quantalogic/generative_model.py,sha256=s94heYFPSA2IO4E3ay3CF1k9hXKNAzX1KVmxHlQgF_8,12397
9
+ quantalogic/generative_model.py,sha256=vv7fvkZz3GRvtuhYGeq64bJUhxyjmhbz-G5mDbsvCiE,15451
10
10
  quantalogic/interactive_text_editor.py,sha256=kYeTA2qej5kxtPvAUHy_Dr2MhrGQAyenLFpW9mU9Rmw,6855
11
- quantalogic/main.py,sha256=VTJ8kbhirebijVSujgkqO-w3dK9BB4me0ZF07CFclas,16731
11
+ quantalogic/main.py,sha256=9jymO-5P0lFlMVTRRaZde1jO9aeZtwRipH2qQbS91BQ,16777
12
12
  quantalogic/memory.py,sha256=zbtRuM05jaS2lJll-92dt5JfYVLERnF_m_9xqp2x-k0,6304
13
13
  quantalogic/model_names.py,sha256=UZlz25zG9B2dpfwdw_e1Gw5qFsKQ7iME9FJh9Ts4u6s,938
14
14
  quantalogic/prompts.py,sha256=CW4CRgW1hTpXeWdeJNbPaRPUeUm-xKuGHJrT8mOtvkw,3602
@@ -22,8 +22,9 @@ quantalogic/server/static/js/event_visualizer.js,sha256=eFkkWyNZw3zOZlF18kxbfsWq
22
22
  quantalogic/server/static/js/quantalogic.js,sha256=x7TrlZGR1Y0WLK2DWl1xY847BhEWMPnL0Ua7KtOldUc,22311
23
23
  quantalogic/server/templates/index.html,sha256=nDnXJoQEm1vXbhXtgaYk0G5VXj0wwzE6KrqEDhHFpj4,7773
24
24
  quantalogic/tool_manager.py,sha256=JAC5E5kLfYzYJx0QRIWbG14q1hlkOcwJFBG7HE8twpU,2425
25
- quantalogic/tools/__init__.py,sha256=8JYoIGWTtFn-EjSK3gdUv9Bl8vuUTmfmbQwDhD-k0d8,1789
25
+ quantalogic/tools/__init__.py,sha256=GcYjE1r6aNQ_JZ8uwk0yaCCCMBz6zrD_PjkRtZiUhSk,1923
26
26
  quantalogic/tools/agent_tool.py,sha256=MXCXxWHRch7VK4UWhtRP1jeI8Np9Ne2CUGo8vm1oZiM,3064
27
+ quantalogic/tools/dalle_e.py,sha256=nur2kl6DKjaWWaHcmF_y9vS5bvty2fW8hQfdgf5KWfs,10948
27
28
  quantalogic/tools/download_http_file_tool.py,sha256=wTfanbXjIRi5-qrbluuLvNmDNhvmYAnlMVb3dO8C2ss,2210
28
29
  quantalogic/tools/duckduckgo_search_tool.py,sha256=xVaEb_SUK5NL3lwMQXj1rGQYYvNT-td-qaB9QCes27Q,7014
29
30
  quantalogic/tools/edit_whole_content_tool.py,sha256=nXmpAvojvqvAcqNMy1kUKZ1ocboky_ZcnCR4SNCSPgw,2360
@@ -48,7 +49,8 @@ quantalogic/tools/markitdown_tool.py,sha256=lpbJBLx43_x2DjiZAV1HSidkHeqkkV0KvgeL
48
49
  quantalogic/tools/nodejs_tool.py,sha256=zdnE0VFj_5786uR2L0o-SKR0Gk8L-U7rdj7xGHJYIq0,19905
49
50
  quantalogic/tools/python_tool.py,sha256=70HLbfU2clOBgj4axDOtIKzXwEBMNGEAX1nGSf-KNNQ,18156
50
51
  quantalogic/tools/read_file_block_tool.py,sha256=FTcDAUOOPQOvWRjnRI6nMI1Upus90klR4PC0pbPP_S8,5266
51
- quantalogic/tools/read_file_tool.py,sha256=bOWJbA0GU-hYbFOJ-tQVlSVz0r6WrVAfzy4aXOnAcBw,2757
52
+ quantalogic/tools/read_file_tool.py,sha256=l6k-SOIV9krpXAmUTkxzua51S-KHgzGqkcDlD5AD8K0,2710
53
+ quantalogic/tools/read_html_tool.py,sha256=Vq2rHY8a36z1-4rN6c_kYjPUTQ4I2UT154PMpaoWSkA,11139
52
54
  quantalogic/tools/replace_in_file_tool.py,sha256=n63s09Y8RXOKGjxfWw0D6F6JpQ6ERSJxVJOzmceVXLk,12953
53
55
  quantalogic/tools/ripgrep_tool.py,sha256=sRzHaWac9fa0cCGhECJN04jw_Ko0O3u45KDWzMIYcvY,14291
54
56
  quantalogic/tools/search_definition_names.py,sha256=Qj9ex226vHs8Jf-kydmTh7B_R8O5buIsJpQu3CvYw7k,18601
@@ -71,8 +73,8 @@ quantalogic/utils/read_http_text_content.py,sha256=n3IayT5KcqctIVVF2gOQQAMf3Ow6e
71
73
  quantalogic/version.py,sha256=ea_cRutaQk5_lwlLbUUvPFuOT7Of7-gAsDl7wdveS-g,107
72
74
  quantalogic/xml_parser.py,sha256=uMLQNHTRCg116FwcjRoquZmSwVtE4LEH-6V2E3RD-dA,11466
73
75
  quantalogic/xml_tool_parser.py,sha256=Vz4LEgDbelJynD1siLOVkJ3gLlfHsUk65_gCwbYJyGc,3784
74
- quantalogic-0.2.22.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
75
- quantalogic-0.2.22.dist-info/METADATA,sha256=fsOAHh2H8db1iZnejR9VbIx5vTUer0uf_p8AJkjHbw8,20079
76
- quantalogic-0.2.22.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
77
- quantalogic-0.2.22.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
78
- quantalogic-0.2.22.dist-info/RECORD,,
76
+ quantalogic-0.2.23.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
77
+ quantalogic-0.2.23.dist-info/METADATA,sha256=RL78oJwIbWXJuTTEgt6blHokOqiDpX6G3SdCy3i-ZFk,20174
78
+ quantalogic-0.2.23.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
79
+ quantalogic-0.2.23.dist-info/entry_points.txt,sha256=h74O_Q3qBRCrDR99qvwB4BpBGzASPUIjCfxHq6Qnups,183
80
+ quantalogic-0.2.23.dist-info/RECORD,,