auto-coder 0.1.347__py3-none-any.whl → 0.1.349__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 auto-coder might be problematic. Click here for more details.

Files changed (37) hide show
  1. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/RECORD +37 -27
  3. autocoder/auto_coder_runner.py +19 -14
  4. autocoder/chat_auto_coder_lang.py +5 -3
  5. autocoder/common/auto_coder_lang.py +3 -3
  6. autocoder/common/model_speed_tester.py +392 -0
  7. autocoder/common/printer.py +7 -8
  8. autocoder/common/run_cmd.py +247 -0
  9. autocoder/common/test_run_cmd.py +110 -0
  10. autocoder/common/v2/agent/agentic_edit.py +82 -29
  11. autocoder/common/v2/agent/agentic_edit_conversation.py +9 -0
  12. autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +21 -36
  13. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +4 -7
  14. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +2 -5
  15. autocoder/helper/rag_doc_creator.py +141 -0
  16. autocoder/ignorefiles/__init__.py +4 -0
  17. autocoder/ignorefiles/ignore_file_utils.py +63 -0
  18. autocoder/ignorefiles/test_ignore_file_utils.py +91 -0
  19. autocoder/models.py +49 -9
  20. autocoder/plugins/__init__.py +20 -0
  21. autocoder/rag/cache/byzer_storage_cache.py +10 -4
  22. autocoder/rag/cache/file_monitor_cache.py +27 -24
  23. autocoder/rag/cache/local_byzer_storage_cache.py +11 -5
  24. autocoder/rag/cache/local_duckdb_storage_cache.py +203 -128
  25. autocoder/rag/cache/simple_cache.py +56 -37
  26. autocoder/rag/loaders/filter_utils.py +106 -0
  27. autocoder/rag/loaders/image_loader.py +573 -0
  28. autocoder/rag/loaders/pdf_loader.py +3 -3
  29. autocoder/rag/loaders/test_image_loader.py +209 -0
  30. autocoder/rag/qa_conversation_strategy.py +3 -5
  31. autocoder/rag/utils.py +20 -9
  32. autocoder/utils/_markitdown.py +35 -0
  33. autocoder/version.py +1 -1
  34. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/LICENSE +0 -0
  35. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/WHEEL +0 -0
  36. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/entry_points.txt +0 -0
  37. {auto_coder-0.1.347.dist-info → auto_coder-0.1.349.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,573 @@
1
+ import os
2
+ import traceback
3
+ import re
4
+ from PIL import Image
5
+
6
+ try:
7
+ from paddleocr import PaddleOCR
8
+ except ImportError:
9
+ PaddleOCR = None
10
+
11
+ try:
12
+ import paddlex as paddlex_module
13
+ except ImportError:
14
+ paddlex_module = None
15
+
16
+ import byzerllm
17
+ from byzerllm.utils.client import code_utils
18
+ from autocoder.utils.llms import get_single_llm
19
+ from loguru import logger
20
+ from typing import List, Tuple, Optional
21
+ from autocoder.common.text import TextSimilarity
22
+ from pydantic import BaseModel
23
+
24
+
25
+ class ReplaceInFileTool(BaseModel):
26
+ path: str
27
+ diff: str
28
+
29
+
30
+ class ImageLoader:
31
+ """
32
+ A class for loading and processing images, extracting text and tables from them,
33
+ and converting the content to markdown format.
34
+ """
35
+
36
+ @staticmethod
37
+ def parse_diff(diff_content: str) -> List[Tuple[str, str]]:
38
+ """
39
+ Parses the diff content into a list of (search_block, replace_block) tuples.
40
+ """
41
+ blocks = []
42
+ lines = diff_content.splitlines(keepends=True)
43
+ i = 0
44
+ n = len(lines)
45
+
46
+ while i < n:
47
+ line = lines[i]
48
+ if line.strip() == "<<<<<<< SEARCH":
49
+ i += 1
50
+ search_lines = []
51
+ # Accumulate search block
52
+ while i < n and lines[i].strip() != "=======":
53
+ search_lines.append(lines[i])
54
+ i += 1
55
+ if i >= n:
56
+ logger.warning("Unterminated SEARCH block found in diff content.")
57
+ break
58
+ i += 1 # skip '======='
59
+ replace_lines = []
60
+ # Accumulate replace block
61
+ while i < n and lines[i].strip() != ">>>>>>> REPLACE":
62
+ replace_lines.append(lines[i])
63
+ i += 1
64
+ if i >= n:
65
+ logger.warning("Unterminated REPLACE block found in diff content.")
66
+ break
67
+ i += 1 # skip '>>>>>>> REPLACE'
68
+
69
+ search_block = ''.join(search_lines)
70
+ replace_block = ''.join(replace_lines)
71
+ blocks.append((search_block, replace_block))
72
+ else:
73
+ i += 1
74
+
75
+ if not blocks and diff_content.strip():
76
+ logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
77
+ return blocks
78
+
79
+ @staticmethod
80
+ def paddleocr_extract_text(
81
+ file_path,
82
+ lang='ch',
83
+ use_angle_cls=True,
84
+ page_num=10,
85
+ slice_params=None,
86
+ det_model_dir=None,
87
+ rec_model_dir=None,
88
+ **kwargs
89
+ ):
90
+ """
91
+ 使用 PaddleOCR 识别文本,支持图片、PDF、超大图像滑动窗口
92
+
93
+ Args:
94
+ file_path: 图片或PDF路径
95
+ lang: 语言,默认中文
96
+ use_angle_cls: 是否启用方向分类
97
+ page_num: 识别PDF时的最大页数
98
+ slice_params: 超大图像滑动窗口参数 dict
99
+ det_model_dir: 自定义检测模型路径
100
+ rec_model_dir: 自定义识别模型路径
101
+ kwargs: 其他paddleocr参数
102
+ Returns:
103
+ 识别出的纯文本字符串
104
+ """
105
+ if PaddleOCR is None:
106
+ print("paddleocr not installed")
107
+ return ""
108
+
109
+ # 初始化 OCR
110
+ try:
111
+ ocr = PaddleOCR(
112
+ use_angle_cls=use_angle_cls,
113
+ lang=lang,
114
+ page_num=page_num,
115
+ det_model_dir=det_model_dir,
116
+ rec_model_dir=rec_model_dir,
117
+ **kwargs
118
+ )
119
+ except Exception:
120
+ traceback.print_exc()
121
+ return ""
122
+
123
+ try:
124
+ ext = os.path.splitext(file_path)[1].lower()
125
+
126
+ # 处理PDF
127
+ if ext == ".pdf":
128
+ result = ocr.ocr(file_path, cls=True) # result is list of pages, each page is list of lines
129
+ lines = []
130
+ if result and isinstance(result, list):
131
+ for page in result:
132
+ if page and isinstance(page, list):
133
+ for line_info in page: # line_info is [points, (text, confidence)]
134
+ try:
135
+ # Check structure: [points, (text, confidence)]
136
+ if isinstance(line_info, (list, tuple)) and len(line_info) == 2 and \
137
+ isinstance(line_info[1], (list, tuple)) and len(line_info[1]) >= 1:
138
+ txt = line_info[1][0]
139
+ if isinstance(txt, str):
140
+ lines.append(txt)
141
+ else:
142
+ logger.warning(f"Extracted text is not a string in PDF: {txt} (type: {type(txt)}). Skipping.")
143
+ else:
144
+ logger.warning(f"Unexpected line_info structure in PDF: {line_info}. Skipping.")
145
+ except Exception as e:
146
+ logger.warning(f"Error processing line_info in PDF: {line_info}. Error: {e}")
147
+ return "\n".join(lines)
148
+
149
+ # 处理图片
150
+ else: # Image processing
151
+ if slice_params is not None:
152
+ result = ocr.ocr(file_path, cls=True, slice=slice_params)
153
+ else:
154
+ result = ocr.ocr(file_path, cls=True) # result is [[[points, (text, confidence)], ...]] for single image
155
+
156
+ lines = []
157
+ # Standardize handling: PaddleOCR often returns a list containing one item for single images.
158
+ # result = [page_result] where page_result = [[line1_info], [line2_info], ...]
159
+ if result and isinstance(result, list):
160
+ # Heuristic: Treat 'result' as the list of pages directly.
161
+ # This handles both single image wrapped in list and multi-page PDFs consistently.
162
+ page_list = result
163
+
164
+ for page in page_list:
165
+ if page and isinstance(page, list):
166
+ for line_info in page: # line_info is [points, (text, confidence)]
167
+ try:
168
+ # Check structure: [points, (text, confidence)]
169
+ if isinstance(line_info, (list, tuple)) and len(line_info) == 2 and \
170
+ isinstance(line_info[1], (list, tuple)) and len(line_info[1]) >= 1:
171
+ txt = line_info[1][0]
172
+ if isinstance(txt, str):
173
+ lines.append(txt)
174
+ else:
175
+ # Handle potential nested lists in text: join them? Or log?
176
+ if isinstance(txt, list):
177
+ processed_txt = " ".join(map(str, txt))
178
+ logger.warning(f"Extracted text is a list in Image: {txt}. Joined as: '{processed_txt}'.")
179
+ lines.append(processed_txt) # Attempt to join if it's a list of strings/convertibles
180
+ else:
181
+ logger.warning(f"Extracted text is not a string in Image: {txt} (type: {type(txt)}). Skipping.")
182
+ else:
183
+ logger.warning(f"Unexpected line_info structure in Image: {line_info}. Skipping.")
184
+ except Exception as e:
185
+ logger.warning(f"Error processing line_info in Image: {line_info}. Error: {e}")
186
+ return "\n".join(lines)
187
+ except Exception:
188
+ traceback.print_exc()
189
+ return ""
190
+
191
+ @staticmethod
192
+ def paddlex_table_extract_markdown(image_path):
193
+ """
194
+ 使用 PaddleX 表格识别pipeline,抽取表格并转换为markdown格式
195
+
196
+ Args:
197
+ image_path: 图片路径
198
+ Returns:
199
+ markdown格式的表格字符串
200
+ """
201
+ if paddlex_module is None:
202
+ print("paddlex not installed")
203
+ return ""
204
+
205
+ try:
206
+ # 创建 pipeline
207
+ pipeline = paddlex_module.create_pipeline(pipeline='table_recognition')
208
+ # 预测
209
+ outputs = pipeline.predict([image_path])
210
+ if not outputs:
211
+ return ""
212
+
213
+ md_results = []
214
+ for res in outputs:
215
+ # 获取HTML表格
216
+ html = None
217
+ try:
218
+ html = res.to_html() if hasattr(res, "to_html") else None
219
+ except Exception:
220
+ html = None
221
+
222
+ # 如果没有to_html方法,尝试res.print()内容中提取,或跳过
223
+ if html is None:
224
+ try:
225
+ from io import StringIO
226
+ import sys
227
+ buffer = StringIO()
228
+ sys_stdout = sys.stdout
229
+ sys.stdout = buffer
230
+ res.print()
231
+ sys.stdout = sys_stdout
232
+ html = buffer.getvalue()
233
+ except Exception:
234
+ html = ""
235
+
236
+ # 转markdown
237
+ md = ImageLoader.html_table_to_markdown(html)
238
+ md_results.append(md)
239
+
240
+ return "\n\n".join(md_results)
241
+ except Exception:
242
+ traceback.print_exc()
243
+ return ""
244
+
245
+ @staticmethod
246
+ def html_table_to_markdown(html):
247
+ """
248
+ 简单将HTML table转换为markdown table
249
+ """
250
+ try:
251
+ from bs4 import BeautifulSoup
252
+ except ImportError:
253
+ print("BeautifulSoup4 not installed, cannot convert HTML to markdown")
254
+ return ""
255
+
256
+ try:
257
+ soup = BeautifulSoup(html, "html.parser")
258
+ table = soup.find("table")
259
+ if table is None:
260
+ return ""
261
+
262
+ rows = []
263
+ for tr in table.find_all("tr"):
264
+ cells = tr.find_all(["td", "th"])
265
+ row = [cell.get_text(strip=True) for cell in cells]
266
+ rows.append(row)
267
+
268
+ if not rows:
269
+ return ""
270
+
271
+ # 生成markdown
272
+ md_lines = []
273
+ header = rows[0]
274
+ md_lines.append("| " + " | ".join(header) + " |")
275
+ md_lines.append("|" + "|".join(["---"] * len(header)) + "|")
276
+
277
+ for row in rows[1:]:
278
+ md_lines.append("| " + " | ".join(row) + " |")
279
+
280
+ return "\n".join(md_lines)
281
+ except Exception:
282
+ traceback.print_exc()
283
+ return ""
284
+
285
+ @staticmethod
286
+ def extract_replace_in_file_tools(response)->List[ReplaceInFileTool]:
287
+ tools = []
288
+ # Pattern to match replace_in_file tool blocks
289
+ pattern = r'<replace_in_file>\s*<path>(.*?)</path>\s*<diff>(.*?)</diff>\s*</replace_in_file>'
290
+ matches = re.finditer(pattern, response, re.DOTALL)
291
+
292
+ for match in matches:
293
+ path = match.group(1).strip()
294
+ diff = match.group(2).strip()
295
+ tools.append(ReplaceInFileTool(path=path, diff=diff))
296
+
297
+ return tools
298
+
299
+ @staticmethod
300
+ def format_table_in_content(content: str, llm=None) -> str:
301
+ """Format table content from OCR results into markdown format.
302
+
303
+ Args:
304
+ content: The OCR text content that may contain tables
305
+ llm: The language model to use for formatting
306
+
307
+ Returns:
308
+ Formatted content with tables converted to markdown
309
+ """
310
+
311
+ @byzerllm.prompt()
312
+ def _format_table(content: str)->str:
313
+ '''
314
+ # 表格格式化任务
315
+
316
+ 你是一个专业的OCR后处理专家,擅长将OCR识别出的表格数据转换为规范的Markdown表格。
317
+
318
+ ## 输入内容分析
319
+
320
+ OCR识别的表格通常会有以下特点:
321
+ 1. 每个单元格可能被识别为单独的一行
322
+ 2. 表格的行列结构可能不明显
323
+ 3. 可能包含非表格的文本内容
324
+ 4. 可能存在多个表格
325
+
326
+ ## 你的任务
327
+
328
+ 1. 识别内容中的表格数据
329
+ 2. 将表格数据转换为标准Markdown格式
330
+ 3. 保留非表格的文本内容
331
+ 4. 使用replace_in_file工具格式输出结果
332
+
333
+ ## 输出格式
334
+
335
+ 必须使用以下格式输出结果:
336
+
337
+ ```
338
+ <replace_in_file>
339
+ <path>content</path>
340
+ <diff>
341
+ <<<<<<< SEARCH
342
+ [原始表格文本,精确匹配]
343
+ =======
344
+ [转换后的Markdown表格]
345
+ >>>>>>> REPLACE
346
+ </diff>
347
+ </replace_in_file>
348
+ ```
349
+
350
+ ## 示例
351
+
352
+ 原始OCR文本:
353
+ ```
354
+ 下面是库存情况:
355
+ 产品名称
356
+ 价格
357
+ 库存
358
+ 苹果手机
359
+ 8999 352
360
+ 华为平板
361
+ 4599
362
+ 128
363
+ 小米电视
364
+ 3299
365
+ 89
366
+ 可以看到在,整体库存和价格是健康的。
367
+ ```
368
+
369
+ 转换后的输出:
370
+ ```
371
+ <replace_in_file>
372
+ <path>content</path>
373
+ <diff>
374
+ <<<<<<< SEARCH
375
+ 产品名称
376
+ 价格
377
+ 库存
378
+ 苹果手机
379
+ 8999 352
380
+ 华为平板
381
+ 4599
382
+ 128
383
+ 小米电视
384
+ 3299
385
+ 89
386
+ =======
387
+ | 产品名称 | 价格 | 库存 |
388
+ |---------|------|------|
389
+ | 苹果手机 | 8999 | 352 |
390
+ | 华为平板 | 4599 | 128 |
391
+ | 小米电视 | 3299 | 89 |
392
+ >>>>>>> REPLACE
393
+ </diff>
394
+ </replace_in_file>
395
+ ```
396
+
397
+ ## 处理规则
398
+
399
+ 1. 表格识别:
400
+ - 分析行列结构,识别表头和数据行
401
+ - 如果一行中有多个值,可能是一行表格数据
402
+ - 连续的短行可能是表格的单元格
403
+
404
+ 2. Markdown格式:
405
+ - 表头行使用`|`分隔各列
406
+ - 在表头下方添加分隔行`|---|---|---|`
407
+ - 对齐各列数据
408
+ - 保持原始数据的完整性
409
+
410
+ 3. 多表格处理:
411
+ - 为每个表格创建单独的replace_in_file块
412
+ - 保持表格在原文中的相对位置
413
+
414
+ 4. 非表格内容:
415
+ - 保留原始格式
416
+ - 不要修改非表格文本
417
+
418
+ ## 处理以下内容
419
+
420
+ {{content}}
421
+ '''
422
+
423
+ # Run the prompt with the provided content
424
+ tool_response = _format_table.with_llm(llm).run(content)
425
+
426
+ # Extract tools from the response
427
+ tools = ImageLoader.extract_replace_in_file_tools(tool_response)
428
+
429
+ # Process each tool to apply the replacements
430
+ formatted_content = content
431
+ for tool in tools:
432
+ # For in-memory content replacement (not actual file modification)
433
+ # Parse the diff to get search/replace blocks
434
+ blocks = ImageLoader.parse_diff(tool.diff)
435
+ # Apply each replacement to the content
436
+ for search_block, replace_block in blocks:
437
+ # Check if the search_block exists in the content
438
+ if search_block in formatted_content:
439
+ # Replace and verify the replacement occurred
440
+ new_content = formatted_content.replace(search_block, replace_block)
441
+ if new_content == formatted_content:
442
+ logger.warning(f"Replacement failed despite search block found. Search block length: {len(search_block)}")
443
+ print(f"\n=== FAILED SEARCH BLOCK ===\n{search_block}\n=== END FAILED SEARCH BLOCK ===\n")
444
+ formatted_content = new_content
445
+ else:
446
+ # Fallback to similarity matching when exact match fails
447
+ logger.warning(f"Search block not found in content. Trying similarity matching. Search block length: {len(search_block)}")
448
+ print(f"\n=== NOT FOUND SEARCH BLOCK (trying similarity) ===\n{search_block}\n=== END NOT FOUND SEARCH BLOCK ===\n")
449
+
450
+ # Use TextSimilarity to find the best matching window
451
+ similarity, best_window = TextSimilarity(search_block, formatted_content).get_best_matching_window()
452
+ similarity_threshold = 0.8 # Can be adjusted based on needs
453
+
454
+ if similarity > similarity_threshold:
455
+ logger.info(f"Found similar block with similarity {similarity:.2f}")
456
+ print(f"\n=== SIMILAR BLOCK FOUND (similarity: {similarity:.2f}) ===\n{best_window}\n=== END SIMILAR BLOCK ===\n")
457
+ formatted_content = formatted_content.replace(best_window, replace_block, 1)
458
+ else:
459
+ logger.warning(f"No similar block found. Best similarity: {similarity:.2f}")
460
+
461
+ return formatted_content
462
+
463
+ @staticmethod
464
+ def extract_text_from_image(
465
+ image_path: str,
466
+ llm,
467
+ engine: str = "vl",
468
+ product_mode: str = "lite",
469
+ paddle_kwargs: dict = None
470
+ ) -> str:
471
+ """
472
+ 识别图片或PDF中的所有文本内容,包括表格(以markdown table格式)
473
+
474
+ Args:
475
+ image_path: 图片或PDF路径
476
+ llm: LLM对象或字符串(模型名)
477
+ engine: 选择识别引擎
478
+ - "vl": 视觉语言模型
479
+ - "paddle": PaddleOCR
480
+ - "paddle_table": PaddleX表格识别
481
+ product_mode: get_single_llm的参数
482
+ paddle_kwargs: dict,传递给PaddleOCR的参数
483
+ Returns:
484
+ markdown内容字符串
485
+ """
486
+ if isinstance(llm, str):
487
+ llm = get_single_llm(llm, product_mode=product_mode)
488
+
489
+ markdown_content = ""
490
+
491
+ if engine == "vl":
492
+ try:
493
+ vl_model = llm.get_sub_client("vl_model") if llm.get_sub_client("vl_model") else llm
494
+
495
+ @byzerllm.prompt()
496
+ def analyze_image(image_path):
497
+ """
498
+ {{ image }}
499
+ 你是一名图像理解专家,请识别这张图片中的所有内容,优先识别文字和表格。
500
+ 对于普通文字,输出为段落文本。
501
+ 对于表格截图,转换成markdown table格式输出。
502
+ 请根据内容顺序,整合成一份markdown文档。
503
+ 只返回markdown内容,不要添加额外解释。
504
+ """
505
+ image = byzerllm.Image.load_image_from_path(image_path)
506
+ return {"image": image}
507
+
508
+ result = analyze_image.with_llm(vl_model).run(image_path)
509
+ md_blocks = code_utils.extract_code(result, language="markdown")
510
+ if md_blocks:
511
+ markdown_content = md_blocks[-1][1]
512
+ else:
513
+ markdown_content = result.strip()
514
+ if not markdown_content:
515
+ raise ValueError("Empty markdown from vl_model")
516
+ return markdown_content
517
+
518
+ except Exception:
519
+ traceback.print_exc()
520
+ return ""
521
+
522
+ elif engine == "paddle":
523
+ if paddle_kwargs is None:
524
+ paddle_kwargs = {}
525
+
526
+ markdown_content = ImageLoader.paddleocr_extract_text(image_path, **paddle_kwargs)
527
+ return markdown_content
528
+
529
+ elif engine == "paddle_table":
530
+ markdown_content = ImageLoader.paddlex_table_extract_markdown(image_path)
531
+ return markdown_content
532
+
533
+ else:
534
+ print(f"Unknown engine type: {engine}. Supported engines are 'vl', 'paddle', and 'paddle_table'.")
535
+ return ""
536
+
537
+ @staticmethod
538
+ def image_to_markdown(
539
+ image_path: str,
540
+ llm,
541
+ engine: str = "vl",
542
+ product_mode: str = "lite",
543
+ paddle_kwargs: dict = None
544
+ ) -> str:
545
+ """
546
+ 识别图片或PDF内容,生成markdown文件
547
+
548
+ Args:
549
+ image_path: 文件路径
550
+ llm: LLM对象或字符串
551
+ engine: 'vl'、'paddle'或'paddle_table'
552
+ product_mode: LLM参数
553
+ paddle_kwargs: dict,传递给PaddleOCR参数
554
+ Returns:
555
+ markdown内容字符串
556
+ """
557
+ md_content = ImageLoader.extract_text_from_image(
558
+ image_path,
559
+ llm,
560
+ engine=engine,
561
+ product_mode=product_mode,
562
+ paddle_kwargs=paddle_kwargs
563
+ )
564
+
565
+ md_path = os.path.splitext(image_path)[0] + ".md"
566
+ try:
567
+ with open(md_path, "w", encoding="utf-8") as f:
568
+ f.write(md_content)
569
+ except Exception:
570
+ traceback.print_exc()
571
+
572
+ return md_content
573
+
@@ -14,9 +14,9 @@ def extract_text_from_pdf_old(file_path):
14
14
  text += page.extract_text()
15
15
  return text
16
16
 
17
- def extract_text_from_pdf(file_path):
18
- try:
19
- md_converter = MarkItDown()
17
+ def extract_text_from_pdf(file_path, llm=None, product_mode="lite"):
18
+ try:
19
+ md_converter = MarkItDown(llm=llm, product_mode=product_mode)
20
20
  result = md_converter.convert(file_path)
21
21
  return result.text_content
22
22
  except (BaseException, Exception) as e: