realtimex-deeptutor 0.5.0.post3__py3-none-any.whl → 0.5.0.post4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: realtimex-deeptutor
3
- Version: 0.5.0.post3
3
+ Version: 0.5.0.post4
4
4
  Summary: RealTimeX DeepTutor - Intelligent learning companion with multi-agent collaboration and LightRAG
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,5 +1,5 @@
1
1
  realtimex_deeptutor/__init__.py,sha256=sSfuCLjJa6BnayszcU4azNl_sr1OzuKgLP10BAtdoh8,1567
2
- realtimex_deeptutor-0.5.0.post3.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
+ realtimex_deeptutor-0.5.0.post4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
3
3
  scripts/__init__.py,sha256=mxMsCbci-Qon3qWU1JIi93-tYlHAy0NIUbDRmAPVcg0,54
4
4
  scripts/audit_prompts.py,sha256=Ltuk7tvsjpKhiobVbYq1volgVFKiVLgSTaE_Is4MGaM,5651
5
5
  scripts/check_install.py,sha256=GbApEcDLJ6r0QmYrCVHAFCOK4wolpSLwL3eBRmmD3og,13929
@@ -7,7 +7,7 @@ scripts/generate_roster.py,sha256=COsJ12bvZ5W9TI-wAvKpknKBgHr9uQTvJ_JCz2gVMVo,12
7
7
  scripts/install_all.py,sha256=u-A3eLhk1ua_KCjz8WZMkrVNJN6QdYs7NhGOcsm-Mks,23875
8
8
  scripts/migrate_kb.py,sha256=uyJgplkJag35rT2RrwSiT37__gpB4TiA0xh5uVcWIa4,19667
9
9
  scripts/start.py,sha256=EYbyjryor0DN_WcxQMSkKWCboM9UjMkv61fWhLyv63I,30300
10
- scripts/start_web.py,sha256=vzn7TiW7g2RNpAKYPQjHVNtOt6G9MxokEnZE_YWfHu4,22980
10
+ scripts/start_web.py,sha256=aZ5nqH-h2F6I_tAsY-_uy56jIS5ZJt8Fsjw0OHjEYGc,29755
11
11
  scripts/sync_prompts_from_en.py,sha256=TkBSFilYSwnwo0a3cgRnJ84i02zByAIW12N3ePzBwE8,4677
12
12
  src/__init__.py,sha256=UNw3C20mbskiQF3rK3HhjglrG8snhfuiVthc5UsoHX0,1046
13
13
  src/agents/__init__.py,sha256=IPhP4RZnCH2kcUDBkdKHO_ciVdyWnuHUCG2flG5Ydcw,885
@@ -150,7 +150,7 @@ src/api/utils/notebook_manager.py,sha256=4zTn_J10BmWlCaCJo3bcNWiWUGgLw4hGR92sfj3
150
150
  src/api/utils/progress_broadcaster.py,sha256=u1cfxZ2Rek9tSP9sP1hdQMQkh6Dr_6fhzJjBUvFf-3I,2772
151
151
  src/api/utils/task_id_manager.py,sha256=E59dJ2rg-_qY7uACNT3Nmzey0fnpZgfMYerW-8QsaNM,3687
152
152
  src/cli/__init__.py,sha256=MQ18rJWUlLlk_keWk7lrCjOYW5c_kDfJr5roKDu5ZT8,269
153
- src/cli/start.py,sha256=KNQ4ulAB4QCcS39Z5TcYWw4UtTzfEFCJg_yZOtg-Brw,6664
153
+ src/cli/start.py,sha256=g2kYPuynTDCDL66wJ8IUchRLd-ZV8q5-SjAuuZWyYto,6744
154
154
  src/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
155
  src/config/accessors.py,sha256=A06OaUK78MdgLu__6MRcKcVoog1ctDj5fHP4APOyRiI,534
156
156
  src/config/constants.py,sha256=C3370U6qkFExKkoNyDDLcf05FIJU0_5MHvpT6mvDQ7A,734
@@ -289,8 +289,8 @@ src/utils/error_utils.py,sha256=ME_9q-DlmxFl-Xvv3ETPZE_iP705x6MXiuAREgWYsjM,2262
289
289
  src/utils/json_parser.py,sha256=M_KfrsrNvQPSiFvpKHQV79Aj85_MEcLVc6hnKzvTV58,3243
290
290
  src/utils/realtimex.py,sha256=WlGeZ7d-ymXIJ6ZYK2nhjqxArV_7rVHHJLCqaNkcOUw,8648
291
291
  src/utils/network/circuit_breaker.py,sha256=BtjogK5R3tG8fuJniS5-PJKZMtwD5P2SkP2JFiQ9sRA,2722
292
- realtimex_deeptutor-0.5.0.post3.dist-info/METADATA,sha256=cPG-bhP_0bk97uBwQaN9OzVTH9YiVceBLDrNM4_yv8Q,58304
293
- realtimex_deeptutor-0.5.0.post3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
- realtimex_deeptutor-0.5.0.post3.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
- realtimex_deeptutor-0.5.0.post3.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
- realtimex_deeptutor-0.5.0.post3.dist-info/RECORD,,
292
+ realtimex_deeptutor-0.5.0.post4.dist-info/METADATA,sha256=Jjy3Gnux_cDuNru960f4mpCvK3kz1OJWaD7vrd_413k,58304
293
+ realtimex_deeptutor-0.5.0.post4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
+ realtimex_deeptutor-0.5.0.post4.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
+ realtimex_deeptutor-0.5.0.post4.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
+ realtimex_deeptutor-0.5.0.post4.dist-info/RECORD,,
scripts/start_web.py CHANGED
@@ -69,6 +69,194 @@ else:
69
69
  return False
70
70
 
71
71
 
72
+ def check_port_in_use(port: int) -> tuple[bool, int | None]:
73
+ """
74
+ Check if a port is in use and return the PID of the process using it.
75
+
76
+ Uses connect test to check if something is actually LISTENING on the port,
77
+ rather than bind test which fails for TIME_WAIT state.
78
+
79
+ Args:
80
+ port: Port number to check
81
+
82
+ Returns:
83
+ Tuple of (is_in_use, pid_or_none)
84
+ """
85
+ import socket
86
+
87
+ # Use connect test to check if something is actually listening
88
+ # This avoids false positives from TIME_WAIT state
89
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
90
+ sock.settimeout(1)
91
+ try:
92
+ result = sock.connect_ex(("localhost", port))
93
+ if result != 0:
94
+ # Connection refused = nothing listening = port is free
95
+ return False, None
96
+ except (OSError, socket.timeout):
97
+ # Connection failed = port is free
98
+ return False, None
99
+ finally:
100
+ try:
101
+ sock.close()
102
+ except Exception:
103
+ pass
104
+
105
+ # Port is in use (connection succeeded), try to find the PID
106
+ pid = None
107
+ try:
108
+ if os.name == "nt":
109
+ # Windows: use netstat
110
+ result = subprocess.run(
111
+ ["netstat", "-ano"],
112
+ capture_output=True,
113
+ text=True,
114
+ timeout=5,
115
+ )
116
+ for line in result.stdout.splitlines():
117
+ if f":{port}" in line and "LISTENING" in line:
118
+ parts = line.split()
119
+ if parts:
120
+ try:
121
+ pid = int(parts[-1])
122
+ break
123
+ except ValueError:
124
+ pass
125
+ else:
126
+ # Unix: use lsof
127
+ result = subprocess.run(
128
+ ["lsof", "-ti", f":{port}"],
129
+ capture_output=True,
130
+ text=True,
131
+ timeout=5,
132
+ )
133
+ if result.returncode == 0 and result.stdout.strip():
134
+ # May return multiple PIDs, take the first one
135
+ try:
136
+ pid = int(result.stdout.strip().split()[0])
137
+ except (ValueError, IndexError):
138
+ pass
139
+ except Exception:
140
+ pass
141
+
142
+ return True, pid
143
+
144
+
145
+ def kill_process_on_port(port: int, force: bool = False) -> bool:
146
+ """
147
+ Kill the process using a specific port.
148
+
149
+ Args:
150
+ port: Port number
151
+ force: If True, use SIGKILL instead of SIGTERM
152
+
153
+ Returns:
154
+ True if process was killed successfully
155
+ """
156
+ in_use, pid = check_port_in_use(port)
157
+ if not in_use:
158
+ return True # Port is free
159
+
160
+ if pid is None:
161
+ print_flush(f"⚠️ Port {port} is in use but couldn't identify the process")
162
+ return False
163
+
164
+ print_flush(f" Stopping process {pid} on port {port}...")
165
+
166
+ try:
167
+ if os.name == "nt":
168
+ subprocess.run(["taskkill", "/F", "/PID", str(pid)], check=True, capture_output=True)
169
+ else:
170
+ sig = signal.SIGKILL if force else signal.SIGTERM
171
+ os.kill(pid, sig)
172
+ # Wait a moment for process to terminate
173
+ time.sleep(0.5)
174
+ # Check if still running, force kill if needed
175
+ if not force:
176
+ try:
177
+ os.kill(pid, 0) # Check if process exists
178
+ os.kill(pid, signal.SIGKILL)
179
+ time.sleep(0.3)
180
+ except ProcessLookupError:
181
+ pass # Process already terminated
182
+
183
+ # Verify port is now free
184
+ time.sleep(0.3)
185
+ in_use, _ = check_port_in_use(port)
186
+ if not in_use:
187
+ print_flush(f"✅ Port {port} is now free")
188
+ return True
189
+ else:
190
+ print_flush(f"⚠️ Port {port} still in use after killing process")
191
+ return False
192
+
193
+ except Exception as e:
194
+ print_flush(f"❌ Failed to kill process {pid}: {e}")
195
+ return False
196
+
197
+
198
+ def ensure_ports_available(backend_port: int, frontend_port: int, auto_kill: bool = False) -> bool:
199
+ """
200
+ Ensure required ports are available, optionally killing existing processes.
201
+
202
+ Args:
203
+ backend_port: Backend port number
204
+ frontend_port: Frontend port number
205
+ auto_kill: If True, automatically kill processes using the ports
206
+
207
+ Returns:
208
+ True if all ports are available
209
+ """
210
+ ports_to_check = [
211
+ (backend_port, "Backend"),
212
+ (frontend_port, "Frontend"),
213
+ ]
214
+
215
+ conflicts = []
216
+ for port, name in ports_to_check:
217
+ in_use, pid = check_port_in_use(port)
218
+ if in_use:
219
+ conflicts.append((port, name, pid))
220
+
221
+ if not conflicts:
222
+ return True
223
+
224
+ print_flush("")
225
+ print_flush("⚠️ Port conflict detected:")
226
+ for port, name, pid in conflicts:
227
+ pid_info = f" (PID: {pid})" if pid else ""
228
+ print_flush(f" - {name} port {port} is already in use{pid_info}")
229
+
230
+ if auto_kill:
231
+ print_flush("")
232
+ print_flush("🔄 AUTO_KILL_PORTS is enabled, cleaning up...")
233
+ all_freed = True
234
+ for port, name, _ in conflicts:
235
+ if not kill_process_on_port(port):
236
+ all_freed = False
237
+ return all_freed
238
+ else:
239
+ print_flush("")
240
+ print_flush("💡 To resolve this, you can either:")
241
+ print_flush(" 1. Set AUTO_KILL_PORTS=true to automatically clean up")
242
+ print_flush(" 2. Manually kill the processes:")
243
+ for port, name, pid in conflicts:
244
+ if pid:
245
+ if os.name == "nt":
246
+ print_flush(f" taskkill /F /PID {pid}")
247
+ else:
248
+ print_flush(f" kill -9 {pid}")
249
+ else:
250
+ if os.name == "nt":
251
+ print_flush(f" netstat -ano | findstr :{port}")
252
+ else:
253
+ print_flush(f" lsof -ti :{port} | xargs kill -9")
254
+ print_flush(" 3. Use different ports via environment variables:")
255
+ print_flush(" BACKEND_PORT=8002 FRONTEND_PORT=3783 uvx realtimex-deeptutor")
256
+ print_flush("")
257
+ return False
258
+
259
+
72
260
  def terminate_process_tree(process, name="Process", timeout=5):
73
261
  """
74
262
  Terminate a process and all its children (process group).
@@ -541,6 +729,23 @@ if __name__ == "__main__":
541
729
  print_flush(f"⚠️ Warning: Failed to initialize user directories: {e}")
542
730
  print_flush(" Continuing anyway...")
543
731
 
732
+ # Check for port conflicts before starting services
733
+ try:
734
+ from pathlib import Path
735
+
736
+ from src.services.setup import get_ports
737
+
738
+ backend_port, frontend_port = get_ports(
739
+ Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
740
+ )
741
+
742
+ auto_kill = os.environ.get("AUTO_KILL_PORTS", "").lower() in ("true", "1", "yes")
743
+ if not ensure_ports_available(backend_port, frontend_port, auto_kill=auto_kill):
744
+ sys.exit(1)
745
+ except Exception as e:
746
+ print_flush(f"⚠️ Warning: Failed to check ports: {e}")
747
+ print_flush(" Continuing anyway...")
748
+
544
749
  backend = None
545
750
  frontend = None
546
751
 
src/cli/start.py CHANGED
@@ -39,6 +39,7 @@ Examples:
39
39
  Environment Variables:
40
40
  FRONTEND_PORT Frontend port (default: 3782)
41
41
  BACKEND_PORT Backend port (default: 8001)
42
+ AUTO_KILL_PORTS Auto-kill processes using required ports (default: false)
42
43
  RTX_APP_ID RealTimeX App ID (auto-detected)
43
44
  API_BASE_URL Backend API URL (auto-configured)
44
45
  LOG_LEVEL Logging level (DEBUG, INFO, WARNING, ERROR)