auto-coder 0.1.396__py3-none-any.whl → 0.1.398__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 (31) hide show
  1. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/RECORD +31 -12
  3. autocoder/auto_coder_rag.py +1 -0
  4. autocoder/chat_auto_coder.py +3 -0
  5. autocoder/common/conversations/__init__.py +84 -39
  6. autocoder/common/conversations/backup/__init__.py +14 -0
  7. autocoder/common/conversations/backup/backup_manager.py +564 -0
  8. autocoder/common/conversations/backup/restore_manager.py +546 -0
  9. autocoder/common/conversations/cache/__init__.py +16 -0
  10. autocoder/common/conversations/cache/base_cache.py +89 -0
  11. autocoder/common/conversations/cache/cache_manager.py +368 -0
  12. autocoder/common/conversations/cache/memory_cache.py +224 -0
  13. autocoder/common/conversations/config.py +195 -0
  14. autocoder/common/conversations/exceptions.py +72 -0
  15. autocoder/common/conversations/file_locker.py +145 -0
  16. autocoder/common/conversations/manager.py +917 -0
  17. autocoder/common/conversations/models.py +154 -0
  18. autocoder/common/conversations/search/__init__.py +15 -0
  19. autocoder/common/conversations/search/filter_manager.py +431 -0
  20. autocoder/common/conversations/search/text_searcher.py +366 -0
  21. autocoder/common/conversations/storage/__init__.py +16 -0
  22. autocoder/common/conversations/storage/base_storage.py +82 -0
  23. autocoder/common/conversations/storage/file_storage.py +267 -0
  24. autocoder/common/conversations/storage/index_manager.py +317 -0
  25. autocoder/common/rag_manager/rag_manager.py +16 -18
  26. autocoder/rags.py +74 -24
  27. autocoder/version.py +1 -1
  28. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/LICENSE +0 -0
  29. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/WHEEL +0 -0
  30. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/entry_points.txt +0 -0
  31. {auto_coder-0.1.396.dist-info → auto_coder-0.1.398.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,267 @@
1
+ """
2
+ 文件存储实现
3
+
4
+ 基于JSON文件的对话存储实现,支持原子写入和数据完整性检查。
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import tempfile
10
+ import re
11
+ from typing import Optional, List, Dict, Any
12
+ from pathlib import Path
13
+
14
+ from .base_storage import BaseStorage
15
+ from ..exceptions import DataIntegrityError
16
+
17
+
18
+ class FileStorage(BaseStorage):
19
+ """基于文件的存储实现"""
20
+
21
+ def __init__(self, storage_path: str):
22
+ """
23
+ 初始化文件存储
24
+
25
+ Args:
26
+ storage_path: 存储目录路径
27
+ """
28
+ self.storage_path = Path(storage_path)
29
+ self._ensure_storage_directory()
30
+
31
+ def _ensure_storage_directory(self):
32
+ """确保存储目录存在"""
33
+ self.storage_path.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _get_conversation_file_path(self, conversation_id: str) -> Path:
36
+ """
37
+ 获取对话文件路径
38
+
39
+ Args:
40
+ conversation_id: 对话ID
41
+
42
+ Returns:
43
+ Path: 对话文件路径
44
+ """
45
+ # 清理文件名中的特殊字符
46
+ safe_filename = self._sanitize_filename(conversation_id)
47
+ return self.storage_path / f"{safe_filename}.json"
48
+
49
+ def _sanitize_filename(self, filename: str) -> str:
50
+ """
51
+ 清理文件名,移除或替换特殊字符
52
+
53
+ Args:
54
+ filename: 原始文件名
55
+
56
+ Returns:
57
+ str: 安全的文件名
58
+ """
59
+ # 移除或替换不安全的字符
60
+ safe_filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
61
+ # 确保文件名不为空
62
+ if not safe_filename or safe_filename.isspace():
63
+ safe_filename = 'unnamed'
64
+ return safe_filename
65
+
66
+ def _validate_conversation_data(self, conversation_data: Dict[str, Any]) -> bool:
67
+ """
68
+ 验证对话数据的完整性
69
+
70
+ Args:
71
+ conversation_data: 对话数据
72
+
73
+ Returns:
74
+ bool: 数据有效返回True
75
+ """
76
+ if not isinstance(conversation_data, dict):
77
+ return False
78
+
79
+ # 检查必需字段
80
+ required_fields = ['conversation_id']
81
+ for field in required_fields:
82
+ if field not in conversation_data:
83
+ return False
84
+ if not conversation_data[field]:
85
+ return False
86
+
87
+ return True
88
+
89
+ def _atomic_write_file(self, file_path: Path, data: Dict[str, Any]) -> bool:
90
+ """
91
+ 原子写入文件
92
+
93
+ Args:
94
+ file_path: 目标文件路径
95
+ data: 要写入的数据
96
+
97
+ Returns:
98
+ bool: 写入成功返回True
99
+ """
100
+ temp_fd = None
101
+ temp_path = None
102
+
103
+ try:
104
+ # 创建临时文件
105
+ temp_fd, temp_path = tempfile.mkstemp(
106
+ suffix='.tmp',
107
+ prefix=file_path.name + '.',
108
+ dir=file_path.parent
109
+ )
110
+
111
+ # 写入数据到临时文件
112
+ with os.fdopen(temp_fd, 'w', encoding='utf-8') as temp_file:
113
+ temp_fd = None # 文件已关闭,避免重复关闭
114
+ json.dump(data, temp_file, ensure_ascii=False, indent=2)
115
+
116
+ # 原子重命名
117
+ os.rename(temp_path, file_path)
118
+ return True
119
+
120
+ except (OSError, IOError, PermissionError, TypeError, ValueError):
121
+ # 清理临时文件
122
+ if temp_fd is not None:
123
+ try:
124
+ os.close(temp_fd)
125
+ except OSError:
126
+ pass
127
+
128
+ if temp_path and os.path.exists(temp_path):
129
+ try:
130
+ os.unlink(temp_path)
131
+ except OSError:
132
+ pass
133
+
134
+ return False
135
+
136
+ def save_conversation(self, conversation_data: Dict[str, Any]) -> bool:
137
+ """
138
+ 保存对话数据
139
+
140
+ Args:
141
+ conversation_data: 对话数据字典
142
+
143
+ Returns:
144
+ bool: 保存成功返回True
145
+ """
146
+ if not self._validate_conversation_data(conversation_data):
147
+ return False
148
+
149
+ conversation_id = conversation_data['conversation_id']
150
+ file_path = self._get_conversation_file_path(conversation_id)
151
+
152
+ return self._atomic_write_file(file_path, conversation_data)
153
+
154
+ def load_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
155
+ """
156
+ 加载对话数据
157
+
158
+ Args:
159
+ conversation_id: 对话ID
160
+
161
+ Returns:
162
+ Optional[Dict[str, Any]]: 对话数据,不存在返回None
163
+ """
164
+ file_path = self._get_conversation_file_path(conversation_id)
165
+
166
+ if not file_path.exists():
167
+ return None
168
+
169
+ try:
170
+ with open(file_path, 'r', encoding='utf-8') as f:
171
+ data = json.load(f)
172
+
173
+ # 验证加载的数据
174
+ if not self._validate_conversation_data(data):
175
+ raise DataIntegrityError(f"对话数据无效: {conversation_id}")
176
+
177
+ return data
178
+
179
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
180
+ raise DataIntegrityError(f"对话文件损坏: {conversation_id}, 错误: {str(e)}")
181
+ except (OSError, IOError) as e:
182
+ # 文件读取错误,返回None
183
+ return None
184
+
185
+ def delete_conversation(self, conversation_id: str) -> bool:
186
+ """
187
+ 删除对话数据
188
+
189
+ Args:
190
+ conversation_id: 对话ID
191
+
192
+ Returns:
193
+ bool: 删除成功返回True
194
+ """
195
+ file_path = self._get_conversation_file_path(conversation_id)
196
+
197
+ if not file_path.exists():
198
+ return False
199
+
200
+ try:
201
+ file_path.unlink()
202
+ return True
203
+ except (OSError, IOError):
204
+ return False
205
+
206
+ def conversation_exists(self, conversation_id: str) -> bool:
207
+ """
208
+ 检查对话是否存在
209
+
210
+ Args:
211
+ conversation_id: 对话ID
212
+
213
+ Returns:
214
+ bool: 存在返回True
215
+ """
216
+ file_path = self._get_conversation_file_path(conversation_id)
217
+ return file_path.exists()
218
+
219
+ def list_conversations(
220
+ self,
221
+ limit: Optional[int] = None,
222
+ offset: int = 0
223
+ ) -> List[Dict[str, Any]]:
224
+ """
225
+ 列出对话
226
+
227
+ Args:
228
+ limit: 限制返回数量
229
+ offset: 偏移量
230
+
231
+ Returns:
232
+ List[Dict[str, Any]]: 对话数据列表
233
+ """
234
+ conversations = []
235
+
236
+ try:
237
+ # 获取所有JSON文件
238
+ json_files = list(self.storage_path.glob("*.json"))
239
+
240
+ # 按修改时间排序(最新的在前)
241
+ json_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
242
+
243
+ # 应用偏移量和限制
244
+ if limit is not None:
245
+ json_files = json_files[offset:offset + limit]
246
+ else:
247
+ json_files = json_files[offset:]
248
+
249
+ # 加载对话数据
250
+ for file_path in json_files:
251
+ try:
252
+ with open(file_path, 'r', encoding='utf-8') as f:
253
+ data = json.load(f)
254
+
255
+ # 验证数据
256
+ if self._validate_conversation_data(data):
257
+ conversations.append(data)
258
+
259
+ except (json.JSONDecodeError, UnicodeDecodeError, OSError, IOError):
260
+ # 跳过损坏的文件
261
+ continue
262
+
263
+ except OSError:
264
+ # 目录访问错误,返回空列表
265
+ pass
266
+
267
+ return conversations
@@ -0,0 +1,317 @@
1
+ """
2
+ 索引管理器实现
3
+
4
+ 提供对话索引管理功能,支持快速查询、搜索和过滤。
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import time
10
+ from typing import Optional, List, Dict, Any
11
+ from pathlib import Path
12
+
13
+ from ..file_locker import FileLocker
14
+ from ..exceptions import DataIntegrityError
15
+
16
+
17
+ class IndexManager:
18
+ """索引管理器,用于管理对话索引"""
19
+
20
+ def __init__(self, index_path: str):
21
+ """
22
+ 初始化索引管理器
23
+
24
+ Args:
25
+ index_path: 索引目录路径
26
+ """
27
+ self.index_path = Path(index_path)
28
+ self.index_file = self.index_path / "conversations.idx"
29
+ self.lock_file = self.index_path / "index.lock"
30
+
31
+ self._ensure_index_directory()
32
+ self._load_index()
33
+
34
+ def _ensure_index_directory(self):
35
+ """确保索引目录存在"""
36
+ self.index_path.mkdir(parents=True, exist_ok=True)
37
+
38
+ def _load_index(self):
39
+ """加载索引数据"""
40
+ try:
41
+ if self.index_file.exists():
42
+ with open(self.index_file, 'r', encoding='utf-8') as f:
43
+ self._index_data = json.load(f)
44
+ else:
45
+ self._index_data = {}
46
+ except (json.JSONDecodeError, OSError, IOError):
47
+ # 如果索引损坏,重建空索引
48
+ self._index_data = {}
49
+
50
+ def _save_index(self) -> bool:
51
+ """
52
+ 保存索引数据
53
+
54
+ Returns:
55
+ bool: 保存成功返回True
56
+ """
57
+ try:
58
+ # 使用临时文件进行原子写入
59
+ temp_file = self.index_file.with_suffix('.tmp')
60
+
61
+ with open(temp_file, 'w', encoding='utf-8') as f:
62
+ json.dump(self._index_data, f, ensure_ascii=False, indent=2)
63
+
64
+ # 原子重命名
65
+ temp_file.replace(self.index_file)
66
+ return True
67
+
68
+ except (OSError, IOError):
69
+ return False
70
+
71
+ def add_conversation(self, conversation_metadata: Dict[str, Any]) -> bool:
72
+ """
73
+ 添加对话到索引
74
+
75
+ Args:
76
+ conversation_metadata: 对话元数据
77
+
78
+ Returns:
79
+ bool: 添加成功返回True
80
+ """
81
+ if not conversation_metadata.get('conversation_id'):
82
+ return False
83
+
84
+ conversation_id = conversation_metadata['conversation_id']
85
+
86
+ try:
87
+ # 重新加载索引以获取最新数据
88
+ self._load_index()
89
+
90
+ # 添加或更新对话元数据
91
+ self._index_data[conversation_id] = conversation_metadata.copy()
92
+
93
+ # 保存索引
94
+ return self._save_index()
95
+
96
+ except Exception:
97
+ return False
98
+
99
+ def update_conversation(self, conversation_metadata: Dict[str, Any]) -> bool:
100
+ """
101
+ 更新索引中的对话
102
+
103
+ Args:
104
+ conversation_metadata: 对话元数据
105
+
106
+ Returns:
107
+ bool: 更新成功返回True
108
+ """
109
+ if not conversation_metadata.get('conversation_id'):
110
+ return False
111
+
112
+ conversation_id = conversation_metadata['conversation_id']
113
+
114
+ try:
115
+ # 重新加载索引以获取最新数据
116
+ self._load_index()
117
+
118
+ # 检查对话是否存在
119
+ if conversation_id not in self._index_data:
120
+ return False
121
+
122
+ # 更新对话元数据
123
+ self._index_data[conversation_id] = conversation_metadata.copy()
124
+
125
+ # 保存索引
126
+ return self._save_index()
127
+
128
+ except Exception:
129
+ return False
130
+
131
+ def remove_conversation(self, conversation_id: str) -> bool:
132
+ """
133
+ 从索引中删除对话
134
+
135
+ Args:
136
+ conversation_id: 对话ID
137
+
138
+ Returns:
139
+ bool: 删除成功返回True
140
+ """
141
+ try:
142
+ # 重新加载索引以获取最新数据
143
+ self._load_index()
144
+
145
+ # 检查对话是否存在
146
+ if conversation_id not in self._index_data:
147
+ return False
148
+
149
+ # 删除对话
150
+ del self._index_data[conversation_id]
151
+
152
+ # 保存索引
153
+ return self._save_index()
154
+
155
+ except Exception:
156
+ return False
157
+
158
+ def get_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
159
+ """
160
+ 从索引获取对话信息
161
+
162
+ Args:
163
+ conversation_id: 对话ID
164
+
165
+ Returns:
166
+ Optional[Dict[str, Any]]: 对话元数据,不存在返回None
167
+ """
168
+ try:
169
+ # 重新加载索引以获取最新数据
170
+ self._load_index()
171
+
172
+ return self._index_data.get(conversation_id)
173
+
174
+ except Exception:
175
+ return None
176
+
177
+ def conversation_exists(self, conversation_id: str) -> bool:
178
+ """
179
+ 检查对话是否在索引中存在
180
+
181
+ Args:
182
+ conversation_id: 对话ID
183
+
184
+ Returns:
185
+ bool: 存在返回True
186
+ """
187
+ return self.get_conversation(conversation_id) is not None
188
+
189
+ def list_conversations(
190
+ self,
191
+ limit: Optional[int] = None,
192
+ offset: int = 0,
193
+ sort_by: str = 'updated_at',
194
+ sort_order: str = 'desc'
195
+ ) -> List[Dict[str, Any]]:
196
+ """
197
+ 列出对话
198
+
199
+ Args:
200
+ limit: 限制返回数量
201
+ offset: 偏移量
202
+ sort_by: 排序字段
203
+ sort_order: 排序顺序 ('asc' 或 'desc')
204
+
205
+ Returns:
206
+ List[Dict[str, Any]]: 对话元数据列表
207
+ """
208
+ try:
209
+ # 重新加载索引以获取最新数据
210
+ self._load_index()
211
+
212
+ # 获取所有对话
213
+ conversations = list(self._index_data.values())
214
+
215
+ # 排序
216
+ reverse = (sort_order.lower() == 'desc')
217
+
218
+ if sort_by == 'name':
219
+ conversations.sort(
220
+ key=lambda x: x.get('name', ''),
221
+ reverse=reverse
222
+ )
223
+ elif sort_by == 'created_at':
224
+ conversations.sort(
225
+ key=lambda x: x.get('created_at', 0),
226
+ reverse=reverse
227
+ )
228
+ elif sort_by == 'updated_at':
229
+ conversations.sort(
230
+ key=lambda x: x.get('updated_at', 0),
231
+ reverse=reverse
232
+ )
233
+
234
+ # 应用分页
235
+ if limit is not None:
236
+ return conversations[offset:offset + limit]
237
+ else:
238
+ return conversations[offset:]
239
+
240
+ except Exception:
241
+ return []
242
+
243
+ def search_conversations(
244
+ self,
245
+ query: Optional[str] = None,
246
+ filters: Optional[Dict[str, Any]] = None
247
+ ) -> List[Dict[str, Any]]:
248
+ """
249
+ 搜索对话
250
+
251
+ Args:
252
+ query: 搜索查询字符串
253
+ filters: 过滤条件
254
+
255
+ Returns:
256
+ List[Dict[str, Any]]: 匹配的对话元数据列表
257
+ """
258
+ try:
259
+ # 重新加载索引以获取最新数据
260
+ self._load_index()
261
+
262
+ conversations = list(self._index_data.values())
263
+ results = []
264
+
265
+ for conv in conversations:
266
+ # 文本搜索
267
+ if query:
268
+ query_lower = query.lower()
269
+ name_match = query_lower in conv.get('name', '').lower()
270
+ desc_match = query_lower in conv.get('description', '').lower()
271
+
272
+ if not (name_match or desc_match):
273
+ continue
274
+
275
+ # 应用过滤器
276
+ if filters:
277
+ match = True
278
+
279
+ # 时间范围过滤
280
+ # created_after: 大于等于这个时间的记录
281
+ if 'created_after' in filters:
282
+ created_at = conv.get('created_at', 0)
283
+ if created_at < filters['created_after']:
284
+ match = False
285
+
286
+ # created_before: 小于这个时间的记录(不包含边界)
287
+ if 'created_before' in filters:
288
+ created_at = conv.get('created_at', float('inf'))
289
+ if created_at >= filters['created_before']:
290
+ match = False
291
+
292
+ # 消息数量过滤
293
+ if 'min_message_count' in filters:
294
+ message_count = conv.get('message_count', 0)
295
+ if message_count < filters['min_message_count']:
296
+ match = False
297
+
298
+ if 'max_message_count' in filters:
299
+ message_count = conv.get('message_count', float('inf'))
300
+ if message_count > filters['max_message_count']:
301
+ match = False
302
+
303
+ if not match:
304
+ continue
305
+
306
+ results.append(conv)
307
+
308
+ # 按相关性或更新时间排序
309
+ results.sort(
310
+ key=lambda x: x.get('updated_at', 0),
311
+ reverse=True
312
+ )
313
+
314
+ return results
315
+
316
+ except Exception:
317
+ return []
@@ -79,24 +79,22 @@ class RAGManager:
79
79
  with open(config_path, 'r', encoding='utf-8') as f:
80
80
  config_data = json.load(f)
81
81
 
82
- for key, item in config_data.items():
83
- try:
84
- # 构造 server_name: http://host:port/v1
85
- host = item.get("host", "127.0.0.1")
86
- port = item.get("port", 8080)
87
- server_name = f"http://{host}:{port}/v1"
88
-
89
- rag_config = RAGConfig(
90
- name=item.get("name", key),
91
- server_name=server_name,
92
- api_key=None, # 全局配置中没有 api_key
93
- description=item.get("description", f"{key} RAG 服务")
94
- )
95
- self.configs.append(rag_config)
96
- logger.info(f"已加载 RAG 配置: {rag_config.name} -> {rag_config.server_name}")
97
- except Exception as e:
98
- logger.error(f"解析全局 RAG 配置项时出错: {e}, 配置项: {item}")
99
-
82
+ if "data" in config_data and isinstance(config_data["data"], list):
83
+ for item in config_data["data"]:
84
+ try:
85
+ rag_config = RAGConfig(
86
+ name=item.get("name", ""),
87
+ server_name=item.get("base_url", ""),
88
+ api_key=item.get("api_key"),
89
+ description=item.get("description")
90
+ )
91
+ self.configs.append(rag_config)
92
+ logger.info(f"已加载 RAG 配置: {rag_config.name} -> {rag_config.server_name}")
93
+ except Exception as e:
94
+ logger.error(f"解析全局 RAG 配置项时出错: {e}, 配置项: {item}")
95
+ else:
96
+ logger.error(f"全局 RAG 配置格式错误,缺少 'data' 字段或 'data' 不是列表")
97
+
100
98
  except json.JSONDecodeError as e:
101
99
  logger.error(f"全局 RAG 配置文件 JSON 格式错误: {e}")
102
100
  except Exception as e: