auto-coder 0.1.302__py3-none-any.whl → 0.1.304__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.

@@ -0,0 +1,327 @@
1
+ """
2
+ Event manager for handling events between System A and System B.
3
+ """
4
+ from loguru import logger
5
+ import os
6
+ import time
7
+ import threading
8
+ from typing import Optional, List, Dict, Any, Callable, Union, TypeVar, Generic
9
+
10
+ from .event_store import EventStore, JsonlEventStore
11
+ from .event_types import Event, EventType, ResponseEvent
12
+
13
+ T = TypeVar('T')
14
+
15
+
16
+ class EventManager:
17
+ """
18
+ Manager for handling events between two systems.
19
+ System A writes events and can be blocked by special events.
20
+ System B reads events and can respond to special events.
21
+ """
22
+
23
+ def __init__(self, event_store: EventStore):
24
+ """
25
+ Initialize an event manager.
26
+
27
+ Args:
28
+ event_store: The event store to use
29
+ """
30
+ self.event_store = event_store
31
+ self._last_read_event_id: Optional[str] = self.event_store._last_event_id
32
+ self._blocking_events: Dict[str, threading.Event] = {}
33
+ self._response_callbacks: Dict[str, Callable[[str], None]] = {}
34
+
35
+ @classmethod
36
+ def create(cls, file_path: str) -> "EventManager":
37
+ """
38
+ Create an event manager with a JSONL event store.
39
+
40
+ Args:
41
+ file_path: Path to the JSONL file
42
+
43
+ Returns:
44
+ An initialized event manager
45
+ """
46
+ store = JsonlEventStore(file_path)
47
+ return cls(store)
48
+
49
+ def write_result(self, content: Union[Dict[str, Any], Any], metadata: Dict[str, Any] = {}) -> Event:
50
+ """
51
+ Write a result event.
52
+
53
+ Args:
54
+ content: The content of the result
55
+ metadata: The metadata of the result
56
+ Returns:
57
+ The created event
58
+ """
59
+ if not isinstance(content, dict):
60
+ content = content.to_dict()
61
+ event = Event(event_type=EventType.RESULT, content=content, metadata=metadata)
62
+ self.event_store.append_event(event)
63
+ return event
64
+
65
+ def write_completion(self, content: Union[Dict[str, Any], Any]) -> Event:
66
+ """
67
+ Write a completion event.
68
+
69
+ Args:
70
+ content: The content of the completion (CompletionContent or dict)
71
+
72
+ Returns:
73
+ The created event
74
+ """
75
+ if not isinstance(content, dict):
76
+ content = content.to_dict()
77
+ event = Event(event_type=EventType.COMPLETION, content=content)
78
+ self.event_store.append_event(event)
79
+ return event
80
+
81
+ def write_error(self, content: Union[Dict[str, Any], Any]) -> Event:
82
+ """
83
+ Write an error event.
84
+
85
+ Args:
86
+ content: The content of the error (ErrorContent or dict)
87
+
88
+ Returns:
89
+ The created event
90
+ """
91
+ if not isinstance(content, dict):
92
+ content = content.to_dict()
93
+ event = Event(event_type=EventType.ERROR, content=content)
94
+ self.event_store.append_event(event)
95
+ return event
96
+
97
+ def write_stream(self, content: Union[Dict[str, Any], Any], metadata: Dict[str, Any] = {}) -> Event:
98
+ """
99
+ Write a stream event.
100
+
101
+ Args:
102
+ content: The content of the stream
103
+ metadata: The metadata of the stream
104
+ Returns:
105
+ The created event
106
+ """
107
+ if not isinstance(content, dict):
108
+ content = content.to_dict()
109
+ event = Event(event_type=EventType.STREAM, content=content, metadata=metadata)
110
+ self.event_store.append_event(event)
111
+ return event
112
+
113
+ def ask_user(self,
114
+ prompt: str,
115
+ options: Optional[List[str]] = None,
116
+ callback: Optional[Callable[[str], None]] = None) -> str:
117
+ """
118
+ Ask the user a question and wait for response.
119
+ This method blocks until a response is received.
120
+
121
+ Args:
122
+ prompt: The question to ask
123
+ options: Optional list of valid responses
124
+ callback: Optional callback function that will be called with the response
125
+
126
+ Returns:
127
+ The user's response
128
+ """
129
+ logger.debug(f"ask_user called with prompt: {prompt}")
130
+ content = {"prompt": prompt}
131
+ if options:
132
+ content["options"] = options
133
+
134
+ event = Event(event_type=EventType.ASK_USER, content=content)
135
+ logger.debug(f"Created ASK_USER event with ID: {event.event_id}")
136
+
137
+ # Register a blocking event
138
+ blocker = threading.Event()
139
+ self.event_store.append_event(event)
140
+ self._blocking_events[event.event_id] = blocker
141
+ logger.debug(f"self._blocking_events: {self._blocking_events}")
142
+ if callback:
143
+ self._response_callbacks[event.event_id] = callback
144
+ logger.debug(f"Registered callback for event: {event.event_id}")
145
+
146
+ # Wait for response
147
+ logger.debug(f"Waiting for response to event: {event.event_id}")
148
+
149
+ # 无限等待响应,不设超时
150
+ wait_result = blocker.wait(timeout=60.0) # 设置一个较长的超时时间(60秒)
151
+
152
+ if not wait_result:
153
+ logger.warning(f"Timeout waiting for response to event: {event.event_id}")
154
+ return ""
155
+
156
+ logger.debug(f"Received signal that response is available for: {event.event_id}")
157
+
158
+ # Get the response event
159
+ def is_response(e: Event) -> bool:
160
+ return (isinstance(e, ResponseEvent) and
161
+ e.event_type == EventType.USER_RESPONSE and
162
+ e.response_to == event.event_id)
163
+
164
+ response_event = self.event_store.wait_for_event(is_response, timeout=1.0)
165
+
166
+ # 获取响应内容
167
+ response = ""
168
+ if response_event and "response" in response_event.content:
169
+ response = response_event.content["response"]
170
+ logger.debug(f"Retrieved response: '{response}' from event: {response_event.event_id}")
171
+ else:
172
+ logger.warning("No valid response found in event store")
173
+
174
+ # 清理资源(移到这里确保在获取响应后才清理)
175
+ if event.event_id in self._blocking_events:
176
+ del self._blocking_events[event.event_id]
177
+ logger.debug(f"Clean up blocker for event: {event.event_id}")
178
+ else:
179
+ logger.debug(f"No blocker found for event: {event.event_id} during cleanup")
180
+
181
+ # 注意:回调不在这里清理,而是在respond_to_user中清理
182
+ # 这样回调函数仍然可用
183
+
184
+ return response
185
+
186
+ def respond_to_user(self, ask_event_id: str, response: str) -> ResponseEvent:
187
+ """
188
+ Respond to a user question.
189
+
190
+ Args:
191
+ ask_event_id: ID of the ASK_USER event
192
+ response: The response to provide
193
+
194
+ Returns:
195
+ The created response event
196
+ """
197
+ logger.debug(f"respond_to_user called for event: {ask_event_id} with response: '{response}'")
198
+
199
+ # 创建响应事件
200
+ event = ResponseEvent(
201
+ event_type=EventType.USER_RESPONSE,
202
+ content={"response": response},
203
+ response_to=ask_event_id
204
+ )
205
+
206
+ # 存储响应事件
207
+ self.event_store.append_event(event)
208
+ logger.debug(f"Response event created and stored with ID: {event.event_id}")
209
+
210
+ # 获取回调和阻塞器
211
+ callback = None
212
+
213
+ # 检查是否有回调
214
+ if ask_event_id in self._response_callbacks:
215
+ callback = self._response_callbacks[ask_event_id]
216
+ # 暂时不删除回调,确保不会丢失
217
+ logger.debug(f"Retrieved callback for event: {ask_event_id}")
218
+ else:
219
+ logger.debug(f"No callback found for event: {ask_event_id}")
220
+
221
+
222
+ # 如果找到了回调,执行它
223
+ if callback:
224
+ try:
225
+ logger.debug(f"Executing callback for event: {ask_event_id}")
226
+ callback(response)
227
+ logger.debug(f"Callback execution completed for event: {ask_event_id}")
228
+
229
+ # 回调执行后再移除它
230
+ if ask_event_id in self._response_callbacks:
231
+ del self._response_callbacks[ask_event_id]
232
+ except Exception as e:
233
+ logger.error(f"Error in response callback: {e}")
234
+
235
+ # 检查是否存在对应的阻塞事件
236
+ logger.debug(f"self._blocking_events: {self._blocking_events}")
237
+ if ask_event_id in self._blocking_events:
238
+ self._blocking_events[ask_event_id].set()
239
+ logger.debug(f"Unblocked event: {ask_event_id}")
240
+ else:
241
+ logger.warning(f"No blocking event found for event_id: {ask_event_id}")
242
+
243
+ return event
244
+
245
+ def read_events(self,
246
+ event_types: Optional[List[EventType]] = None,
247
+ block: bool = False,
248
+ timeout: Optional[float] = None) -> List[Event]:
249
+ """
250
+ Read events from the store.
251
+
252
+ Args:
253
+ event_types: Only get events of these types
254
+ block: Whether to block until new events are available
255
+ timeout: Maximum time to wait if blocking
256
+
257
+ Returns:
258
+ List of events
259
+ """
260
+ events = self.event_store.get_events(
261
+ after_id=self._last_read_event_id,
262
+ event_types=event_types
263
+ )
264
+
265
+ if events:
266
+ self._last_read_event_id = events[-1].event_id
267
+ return events
268
+
269
+ # No events and not blocking
270
+ if not block:
271
+ return []
272
+
273
+ # Wait for new events
274
+ def is_new_event(e: Event) -> bool:
275
+ if event_types and e.event_type not in event_types:
276
+ return False
277
+
278
+ return self._last_read_event_id is None or e.event_id != self._last_read_event_id
279
+
280
+ event = self.event_store.wait_for_event(is_new_event, timeout=timeout)
281
+
282
+ if event:
283
+ events = self.event_store.get_events(
284
+ after_id=self._last_read_event_id,
285
+ event_types=event_types
286
+ )
287
+
288
+ if events:
289
+ self._last_read_event_id = events[-1].event_id
290
+ return events
291
+
292
+ return []
293
+
294
+
295
+ def close(self) -> None:
296
+ """
297
+ Close the event manager and its event store.
298
+ """
299
+ # Unblock any waiting threads
300
+ for blocker in self._blocking_events.values():
301
+ blocker.set()
302
+
303
+ # Close the event store
304
+ if isinstance(self.event_store, JsonlEventStore):
305
+ self.event_store.close()
306
+
307
+ def truncate(self) -> None:
308
+ """
309
+ Clear all events from the event store.
310
+ This truncates the underlying file and resets the last read event ID.
311
+
312
+ Warning: This operation cannot be undone and will permanently delete all events.
313
+ """
314
+ if not hasattr(self.event_store, 'truncate'):
315
+ raise NotImplementedError("The current event store does not support truncation")
316
+
317
+ # Reset the last read event ID
318
+ self._last_read_event_id = None
319
+
320
+ # Clear blocking events and callbacks
321
+ self._blocking_events.clear()
322
+ self._response_callbacks.clear()
323
+
324
+ # Truncate the store
325
+ self.event_store.truncate()
326
+
327
+ logger.debug("Event store has been truncated")
@@ -0,0 +1,245 @@
1
+ import os
2
+ import threading
3
+ from typing import Optional, Dict, Any, List
4
+ import uuid
5
+ from datetime import datetime
6
+ import glob
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+ import byzerllm
11
+
12
+ from .event_manager import EventManager
13
+ from .event_store import EventStore, JsonlEventStore
14
+ from .event_types import Event, EventType
15
+ from loguru import logger
16
+
17
+
18
+ class EventManagerSingleton:
19
+ """
20
+ EventManager的单例包装器。确保整个应用程序中只有一个EventManager实例
21
+ """
22
+ _default_instance: Optional[EventManager] = None
23
+ _instances: Dict[str, EventManager] = {}
24
+ _lock = threading.RLock() # 用于线程安全操作的锁
25
+
26
+ @classmethod
27
+ def get_instance(cls, event_file: Optional[str] = None) -> EventManager:
28
+ """
29
+ Get an EventManager instance for the specified event file.
30
+
31
+ Args:
32
+ event_file: Event file path to use as key. If None, returns the default instance.
33
+
34
+ Returns:
35
+ EventManager: The appropriate EventManager instance
36
+ """
37
+ if event_file is None:
38
+ # Use default instance logic
39
+ if cls._default_instance is None:
40
+ cls._default_instance = EventManager(JsonlEventStore(os.path.join(".auto-coder", "events", "events.jsonl")))
41
+ return cls._default_instance
42
+
43
+ # If event_file is provided, use it as a key to store/retrieve EventManager instances
44
+ if event_file not in cls._instances:
45
+ cls._instances[event_file] = EventManager(JsonlEventStore(event_file))
46
+
47
+ return cls._instances[event_file]
48
+
49
+ @classmethod
50
+ def reset_instance(cls) -> None:
51
+ """
52
+ 重置单例实例。主要用于测试或需要更改事件文件时。
53
+ """
54
+ with cls._lock:
55
+ cls._default_instance = None
56
+
57
+ @classmethod
58
+ def set_default_event_file(cls, event_file: str) -> None:
59
+ """
60
+ 设置默认事件文件路径。
61
+ 仅在实例尚未创建时有效。
62
+
63
+ Args:
64
+ event_file: 新的默认事件文件路径
65
+ """
66
+ if cls._default_instance is None:
67
+ with cls._lock:
68
+ cls._default_event_file = event_file
69
+ else:
70
+ logger.warning("尝试更改默认事件文件,但实例已存在。请先调用reset_instance()。")
71
+
72
+ def get_event_file_path(file_id:str,project_path: Optional[str] = None) -> str:
73
+ if project_path is None:
74
+ return os.path.join(".auto-coder", "events", f"{file_id}.jsonl")
75
+ else:
76
+ return os.path.join(project_path, ".auto-coder", "events", f"{file_id}.jsonl")
77
+
78
+ def gengerate_event_file_path(project_path: Optional[str] = None) -> (str,str):
79
+ """
80
+ 生成一个格式为 uuid_timestamp.jsonl 的事件文件名。
81
+ timestamp 格式为 YYYYMMDD-HHMMSS。
82
+
83
+ Returns:
84
+ 生成的事件文件路径
85
+ """
86
+ # 生成 UUID
87
+ unique_id = str(uuid.uuid4())
88
+
89
+ # 生成当前时间戳,格式为 YYYYMMDD-HHMMSS
90
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
91
+
92
+ file_id = f"{unique_id}_{timestamp}"
93
+ # 组合文件名
94
+ file_name = f"{file_id}.jsonl"
95
+
96
+ if project_path is None:
97
+ return os.path.join(".auto-coder", "events", file_name),file_id
98
+ else:
99
+ return os.path.join(project_path, ".auto-coder", "events", file_name),file_id
100
+
101
+ @byzerllm.prompt()
102
+ def _format_events_prompt(event_files: List[Dict]) -> str:
103
+ """
104
+ <recent_events>
105
+ 最近的事件记录,从最新到最旧排序:
106
+
107
+ {% for event_file in event_files %}
108
+ ## 事件文件 {{ loop.index }}: {{ event_file.file_name }}
109
+
110
+ {% if event_file.timestamp %}
111
+ **时间**: {{ event_file.timestamp }}
112
+ {% endif %}
113
+
114
+ {% if event_file.events %}
115
+ **事件内容**:
116
+ {% for event in event_file.events %}
117
+ - 类型: {{ event.event_type }}
118
+ {% if event.content %}
119
+ 内容:
120
+ ```
121
+ {{ event.content }}
122
+ ```
123
+ {% endif %}
124
+ {% if event.metadata %}
125
+ 元数据: {{ event.metadata }}
126
+ {% endif %}
127
+ {% endfor %}
128
+ {% endif %}
129
+
130
+ {% if not loop.last %}
131
+ ---
132
+ {% endif %}
133
+ {% endfor %}
134
+ </recent_events>
135
+ 请注意上述最近的事件记录,以便更好地理解当前系统状态和交互历史。
136
+ """
137
+
138
+ def to_events_prompt(limit: int = 5, project_path: Optional[str] = None) -> str:
139
+ """
140
+ 获取最近的N条事件文件并读取其中的事件内容,返回格式化后的提示文本。
141
+ 排除类型为 STREAM 的事件,以减少输出内容的冗余。
142
+
143
+ Args:
144
+ limit: 最多返回的事件文件数量,默认为5
145
+ project_path: 可选的项目路径,如果提供则在该路径下查找事件文件
146
+
147
+ Returns:
148
+ 格式化后的事件提示文本
149
+ """
150
+ # 确定事件文件所在目录
151
+ if project_path is None:
152
+ events_dir = os.path.join(".auto-coder", "events")
153
+ else:
154
+ events_dir = os.path.join(project_path, ".auto-coder", "events")
155
+
156
+ # 确保目录存在
157
+ if not os.path.exists(events_dir):
158
+ logger.warning(f"事件目录不存在: {events_dir}")
159
+ return "未找到任何事件记录。"
160
+
161
+ # 获取所有事件文件
162
+ event_file_pattern = os.path.join(events_dir, "*.jsonl")
163
+ event_files = glob.glob(event_file_pattern)
164
+
165
+ if not event_files:
166
+ logger.warning(f"未找到任何事件文件: {event_file_pattern}")
167
+ return "未找到任何事件记录。"
168
+
169
+ # 解析文件名中的时间戳,格式为 uuid_YYYYMMDD-HHMMSS.jsonl
170
+ def extract_timestamp(file_path):
171
+ file_name = os.path.basename(file_path)
172
+ match = re.search(r'_(\d{8}-\d{6})\.jsonl$', file_name)
173
+ if match:
174
+ timestamp_str = match.group(1)
175
+ try:
176
+ return datetime.strptime(timestamp_str, "%Y%m%d-%H%M%S")
177
+ except ValueError:
178
+ return datetime.fromtimestamp(0) # 默认最早时间
179
+ return datetime.fromtimestamp(0) # 默认最早时间
180
+
181
+ # 按时间戳从新到旧排序
182
+ sorted_files = sorted(event_files, key=extract_timestamp, reverse=True)
183
+ limited_files = sorted_files[:limit]
184
+
185
+ # 读取和解析事件文件
186
+ event_data = []
187
+ for file_path in limited_files:
188
+ file_name = os.path.basename(file_path)
189
+ timestamp = extract_timestamp(file_path).strftime("%Y-%m-%d %H:%M:%S")
190
+
191
+ try:
192
+ events = []
193
+ with open(file_path, 'r', encoding='utf-8') as f:
194
+ for line in f:
195
+ line = line.strip()
196
+ if line:
197
+ try:
198
+ event_json = json.loads(line)
199
+ event = Event.from_dict(event_json)
200
+
201
+ # 排除 STREAM 类型的事件
202
+ if event.event_type == EventType.STREAM:
203
+ continue
204
+
205
+ events.append({
206
+ "event_type": event.event_type.name,
207
+ "content": json.dumps(event.content, ensure_ascii=False, indent=2) if event.content else "",
208
+ "metadata": json.dumps(event.metadata, ensure_ascii=False, indent=2) if event.metadata else ""
209
+ })
210
+ except json.JSONDecodeError:
211
+ logger.warning(f"无法解析事件行: {line}")
212
+ except Exception as e:
213
+ logger.warning(f"处理事件时出错: {e}")
214
+
215
+ event_data.append({
216
+ "file_name": file_name,
217
+ "timestamp": timestamp,
218
+ "events": events
219
+ })
220
+ except Exception as e:
221
+ logger.error(f"读取事件文件出错 {file_path}: {e}")
222
+ event_data.append({
223
+ "file_name": file_name,
224
+ "timestamp": timestamp,
225
+ "error": str(e)
226
+ })
227
+
228
+ # 使用模板格式化事件数据
229
+ return _format_events_prompt(event_data)
230
+
231
+ # 便捷函数,可以直接导入使用
232
+ def get_event_manager(event_file: Optional[str] = None) -> EventManager:
233
+ """
234
+ 获取EventManager的单例实例。
235
+
236
+ 如果没有提供event_file,将返回默认的EventManager实例。
237
+ 如果提供了event_file,将返回或创建与该文件关联的EventManager实例。
238
+
239
+ Args:
240
+ event_file: 事件文件路径,如果为None则使用默认路径
241
+
242
+ Returns:
243
+ EventManager实例
244
+ """
245
+ return EventManagerSingleton.get_instance(event_file)