realtimex-deeptutor 0.5.0.post7__py3-none-any.whl → 0.5.0.post9__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.post7
3
+ Version: 0.5.0.post9
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.post7.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
+ realtimex_deeptutor-0.5.0.post9.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=P7YCnJgjva58hjs5F4ogtrlVS1wDLFqWoUfoHZ9vREE,32197
10
+ scripts/start_web.py,sha256=JyFWfDps1P6ErhW8746YC-adODrvQQYQqlNkgLX39pU,35252
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
@@ -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=vs7fAEnJJ4zpAyyBn-7vUmGWiiQvpTWQCRgax1MLTDw,9769
291
291
  src/utils/network/circuit_breaker.py,sha256=BtjogK5R3tG8fuJniS5-PJKZMtwD5P2SkP2JFiQ9sRA,2722
292
- realtimex_deeptutor-0.5.0.post7.dist-info/METADATA,sha256=-pKSs_xYVcVXMMkGETtdGsa7Yabv1jvUCSk_JH1GTMU,58304
293
- realtimex_deeptutor-0.5.0.post7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
- realtimex_deeptutor-0.5.0.post7.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
- realtimex_deeptutor-0.5.0.post7.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
- realtimex_deeptutor-0.5.0.post7.dist-info/RECORD,,
292
+ realtimex_deeptutor-0.5.0.post9.dist-info/METADATA,sha256=H48RMbu9fdvqmkRVyDTK5ODo3vmyyDeIow4MpIoSExg,58304
293
+ realtimex_deeptutor-0.5.0.post9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
+ realtimex_deeptutor-0.5.0.post9.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
+ realtimex_deeptutor-0.5.0.post9.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
+ realtimex_deeptutor-0.5.0.post9.dist-info/RECORD,,
scripts/start_web.py CHANGED
@@ -143,6 +143,103 @@ else:
143
143
  return False
144
144
 
145
145
 
146
+ def find_pid_by_port(port: int) -> int | None:
147
+ """
148
+ Find PID using a port with multiple fallback methods.
149
+ Returns PID or None if not found.
150
+ """
151
+ if os.name == "nt":
152
+ # Windows: use netstat
153
+ try:
154
+ result = subprocess.run(
155
+ ["netstat", "-ano"],
156
+ capture_output=True,
157
+ text=True,
158
+ timeout=5,
159
+ )
160
+ for line in result.stdout.splitlines():
161
+ if f":{port}" in line and "LISTENING" in line:
162
+ parts = line.split()
163
+ if parts:
164
+ try:
165
+ return int(parts[-1])
166
+ except ValueError:
167
+ pass
168
+ except Exception:
169
+ pass
170
+ return None
171
+
172
+ # Unix: Try strategies in order: lsof -> ss -> netstat
173
+
174
+ # Strategy 1: lsof (standard)
175
+ try:
176
+ result = subprocess.run(
177
+ ["lsof", "-ti", f":{port}"],
178
+ capture_output=True,
179
+ text=True,
180
+ timeout=5,
181
+ )
182
+ if result.returncode == 0 and result.stdout.strip():
183
+ try:
184
+ # May return multiple PIDs, take the first one
185
+ return int(result.stdout.strip().split()[0])
186
+ except (ValueError, IndexError):
187
+ pass
188
+ except FileNotFoundError:
189
+ pass # lsof not installed
190
+ except Exception:
191
+ pass
192
+
193
+ # Strategy 2: ss (modern Linux)
194
+ try:
195
+ # ss -lptn 'sport = :8001'
196
+ result = subprocess.run(
197
+ ["ss", "-lptn", f"sport = :{port}"],
198
+ capture_output=True,
199
+ text=True,
200
+ timeout=5,
201
+ )
202
+ if result.returncode == 0:
203
+ # Output format: Users:(("python",pid=1234,fd=3))
204
+ output = result.stdout
205
+ if f":{port}" in output and "pid=" in output:
206
+ import re
207
+
208
+ match = re.search(r"pid=(\d+)", output)
209
+ if match:
210
+ return int(match.group(1))
211
+ except FileNotFoundError:
212
+ pass # ss not installed
213
+ except Exception:
214
+ pass
215
+
216
+ # Strategy 3: netstat (legacy Unix)
217
+ try:
218
+ # netstat -nlp | grep :8001
219
+ p1 = subprocess.Popen(
220
+ ["netstat", "-nlp"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
221
+ )
222
+ p2 = subprocess.Popen(
223
+ ["grep", f":{port}"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True
224
+ )
225
+ if p1.stdout:
226
+ p1.stdout.close()
227
+ output, _ = p2.communicate(timeout=5)
228
+
229
+ if output:
230
+ # Expected: tcp 0 0 0.0.0.0:8001 0.0.0.0:* LISTEN 1234/python
231
+ parts = output.split()
232
+ for part in parts:
233
+ if "/" in part and part.split("/")[0].isdigit():
234
+ return int(part.split("/")[0])
235
+ except FileNotFoundError:
236
+ pass
237
+ except Exception:
238
+ pass
239
+
240
+ return None
241
+
242
+
146
243
  def check_port_in_use(port: int) -> tuple[bool, int | None]:
147
244
  """
148
245
  Check if a port is in use and return the PID of the process using it.
@@ -177,42 +274,7 @@ def check_port_in_use(port: int) -> tuple[bool, int | None]:
177
274
  pass
178
275
 
179
276
  # Port is in use (connection succeeded), try to find the PID
180
- pid = None
181
- try:
182
- if os.name == "nt":
183
- # Windows: use netstat
184
- result = subprocess.run(
185
- ["netstat", "-ano"],
186
- capture_output=True,
187
- text=True,
188
- timeout=5,
189
- )
190
- for line in result.stdout.splitlines():
191
- if f":{port}" in line and "LISTENING" in line:
192
- parts = line.split()
193
- if parts:
194
- try:
195
- pid = int(parts[-1])
196
- break
197
- except ValueError:
198
- pass
199
- else:
200
- # Unix: use lsof
201
- result = subprocess.run(
202
- ["lsof", "-ti", f":{port}"],
203
- capture_output=True,
204
- text=True,
205
- timeout=5,
206
- )
207
- if result.returncode == 0 and result.stdout.strip():
208
- # May return multiple PIDs, take the first one
209
- try:
210
- pid = int(result.stdout.strip().split()[0])
211
- except (ValueError, IndexError):
212
- pass
213
- except Exception:
214
- pass
215
-
277
+ pid = find_pid_by_port(port)
216
278
  return True, pid
217
279
 
218
280
 
@@ -233,7 +295,11 @@ def kill_process_on_port(port: int, force: bool = False) -> bool:
233
295
 
234
296
  if pid is None:
235
297
  print_flush(f"⚠️ Port {port} is in use but couldn't identify the process")
236
- return False
298
+ # Retry detection once with slightly longer delay just in case
299
+ time.sleep(1)
300
+ _, pid = check_port_in_use(port)
301
+ if pid is None:
302
+ return False
237
303
 
238
304
  print_flush(f" Stopping process {pid} on port {port}...")
239
305
 
@@ -241,21 +307,35 @@ def kill_process_on_port(port: int, force: bool = False) -> bool:
241
307
  if os.name == "nt":
242
308
  subprocess.run(["taskkill", "/F", "/PID", str(pid)], check=True, capture_output=True)
243
309
  else:
244
- sig = signal.SIGKILL if force else signal.SIGTERM
245
- os.kill(pid, sig)
310
+ # Try to kill the process group first (handles child processes)
311
+ try:
312
+ pgid = os.getpgid(pid)
313
+ sig = signal.SIGKILL if force else signal.SIGTERM
314
+ os.killpg(pgid, sig)
315
+ except (ProcessLookupError, PermissionError):
316
+ # Fallbck to simple kill if PGID fails
317
+ sig = signal.SIGKILL if force else signal.SIGTERM
318
+ os.kill(pid, sig)
319
+
246
320
  # Wait a moment for process to terminate
247
321
  time.sleep(0.5)
248
322
  # Check if still running, force kill if needed
249
323
  if not force:
250
324
  try:
251
325
  os.kill(pid, 0) # Check if process exists
252
- os.kill(pid, signal.SIGKILL)
326
+ # If still alive, Force Kill
327
+ print_flush(f" Process {pid} still alive, force killing...")
328
+ try:
329
+ pgid = os.getpgid(pid)
330
+ os.killpg(pgid, signal.SIGKILL)
331
+ except:
332
+ os.kill(pid, signal.SIGKILL)
253
333
  time.sleep(0.3)
254
334
  except ProcessLookupError:
255
335
  pass # Process already terminated
256
336
 
257
337
  # Verify port is now free
258
- time.sleep(0.3)
338
+ time.sleep(0.5)
259
339
  in_use, _ = check_port_in_use(port)
260
340
  if not in_use:
261
341
  print_flush(f"✅ Port {port} is now free")
@@ -731,6 +811,15 @@ def _start_frontend_npx(frontend_port, backend_port):
731
811
 
732
812
  npx_cmd = npx_path or "npx"
733
813
 
814
+ # Ensure the directory containing npx (and node) is in PATH
815
+ # This is critical for bundled environments where node might not be in system PATH
816
+ if npx_path:
817
+ npx_dir = os.path.dirname(os.path.abspath(npx_path))
818
+ current_path = env.get("PATH", "")
819
+ # Prepend to PATH to ensure our bundled node is found first
820
+ env["PATH"] = f"{npx_dir}{os.pathsep}{current_path}"
821
+ print_flush(f"📌 Added to PATH: {npx_dir}")
822
+
734
823
  # Process group configuration
735
824
  popen_kwargs = {
736
825
  "shell": False,