realtimex-deeptutor 0.5.0.post6__py3-none-any.whl → 0.5.0.post8__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.post6
3
+ Version: 0.5.0.post8
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.post6.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
2
+ realtimex_deeptutor-0.5.0.post8.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=aZ5nqH-h2F6I_tAsY-_uy56jIS5ZJt8Fsjw0OHjEYGc,29755
10
+ scripts/start_web.py,sha256=ZND4cNGYKpd52hygA2HFJFFiB1IGJoFAgP1mBXUNURA,34796
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.post6.dist-info/METADATA,sha256=ZHgtwKQVopSxjI2xUxf27e-HiHVkrPtObk6b35e2zlk,58304
293
- realtimex_deeptutor-0.5.0.post6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
- realtimex_deeptutor-0.5.0.post6.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
- realtimex_deeptutor-0.5.0.post6.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
- realtimex_deeptutor-0.5.0.post6.dist-info/RECORD,,
292
+ realtimex_deeptutor-0.5.0.post8.dist-info/METADATA,sha256=UZu0J6h0x9m5V-Rn1_dOZYA0FPdgWSm2Na8eKkmeUxQ,58304
293
+ realtimex_deeptutor-0.5.0.post8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
294
+ realtimex_deeptutor-0.5.0.post8.dist-info/entry_points.txt,sha256=slNAzwRLUpqiMtDRZBQIkXbU2vGMHL_om6-o19gYdh8,134
295
+ realtimex_deeptutor-0.5.0.post8.dist-info/top_level.txt,sha256=zUAd6V7jDYhdL7bvg2S38YCM-gVhvd36WqkjxrT-02I,32
296
+ realtimex_deeptutor-0.5.0.post8.dist-info/RECORD,,
scripts/start_web.py CHANGED
@@ -20,6 +20,80 @@ if hasattr(sys.stdout, "reconfigure"):
20
20
  sys.stdout.reconfigure(line_buffering=True)
21
21
 
22
22
 
23
+ class ExecutableResolver:
24
+ """
25
+ Resolves bundled executables (uvx, uv, npx) to their actual paths.
26
+ Ported from ExecutableResolver.js
27
+ """
28
+
29
+ def __init__(self):
30
+ self._resources_root = None
31
+
32
+ def get_resources_root(self) -> Path | None:
33
+ """
34
+ Get the resources root directory (~/.realtimex.ai)
35
+ Returns path to resources root or None if not found
36
+ """
37
+ if self._resources_root:
38
+ return self._resources_root
39
+
40
+ try:
41
+ home = Path.home()
42
+ self._resources_root = home / ".realtimex.ai"
43
+ return self._resources_root
44
+ except Exception as e:
45
+ print_flush(f"⚠️ [ExecutableResolver] Failed to resolve resources directory: {e}")
46
+ return None
47
+
48
+ def resolve_from_candidates(
49
+ self, env_var: str | None, candidates: list[Path | str]
50
+ ) -> str | None:
51
+ """
52
+ Find a valid executable from a list of candidate paths
53
+ """
54
+ search_paths = []
55
+ if env_var:
56
+ search_paths.append(Path(env_var))
57
+
58
+ search_paths.extend([Path(c) for c in candidates])
59
+
60
+ for candidate in search_paths:
61
+ if not candidate:
62
+ continue
63
+ try:
64
+ if candidate.exists() and candidate.is_file():
65
+ return str(candidate)
66
+ except Exception:
67
+ pass
68
+ return None
69
+
70
+ def resolve_npx(self) -> str | None:
71
+ """
72
+ Resolve npx to bundled executable
73
+ """
74
+ resources_dir = self.get_resources_root()
75
+ if not resources_dir:
76
+ return None
77
+
78
+ node_version = os.environ.get("REALTIMEX_NPX_NODE_VERSION", "v22.16.0")
79
+ home = Path.home()
80
+
81
+ candidates = [
82
+ # User's NVM installation (most common case)
83
+ home / ".nvm" / "versions" / "node" / node_version / "bin" / "npx",
84
+ # NVM-installed node in resources dir (bundled)
85
+ resources_dir / ".nvm" / "versions" / "node" / node_version / "bin" / "npx",
86
+ # Windows NVM
87
+ Path("C:/nvm") / node_version / "npx.cmd",
88
+ # Bundled in Resources
89
+ resources_dir / "Resources" / "envs" / "Scripts" / "npx.cmd",
90
+ ]
91
+
92
+ return self.resolve_from_candidates(
93
+ env_var=os.environ.get("REALTIMEX_NPX_PATH"), candidates=candidates
94
+ )
95
+
96
+
23
97
  def print_flush(*args, **kwargs):
24
98
  """Print with flush=True by default"""
25
99
  kwargs.setdefault("flush", True)
@@ -69,6 +143,103 @@ else:
69
143
  return False
70
144
 
71
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
+
72
243
  def check_port_in_use(port: int) -> tuple[bool, int | None]:
73
244
  """
74
245
  Check if a port is in use and return the PID of the process using it.
@@ -103,42 +274,7 @@ def check_port_in_use(port: int) -> tuple[bool, int | None]:
103
274
  pass
104
275
 
105
276
  # 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
-
277
+ pid = find_pid_by_port(port)
142
278
  return True, pid
143
279
 
144
280
 
@@ -159,7 +295,11 @@ def kill_process_on_port(port: int, force: bool = False) -> bool:
159
295
 
160
296
  if pid is None:
161
297
  print_flush(f"⚠️ Port {port} is in use but couldn't identify the process")
162
- 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
163
303
 
164
304
  print_flush(f" Stopping process {pid} on port {port}...")
165
305
 
@@ -167,21 +307,35 @@ def kill_process_on_port(port: int, force: bool = False) -> bool:
167
307
  if os.name == "nt":
168
308
  subprocess.run(["taskkill", "/F", "/PID", str(pid)], check=True, capture_output=True)
169
309
  else:
170
- sig = signal.SIGKILL if force else signal.SIGTERM
171
- 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
+
172
320
  # Wait a moment for process to terminate
173
321
  time.sleep(0.5)
174
322
  # Check if still running, force kill if needed
175
323
  if not force:
176
324
  try:
177
325
  os.kill(pid, 0) # Check if process exists
178
- 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)
179
333
  time.sleep(0.3)
180
334
  except ProcessLookupError:
181
335
  pass # Process already terminated
182
336
 
183
337
  # Verify port is now free
184
- time.sleep(0.3)
338
+ time.sleep(0.5)
185
339
  in_use, _ = check_port_in_use(port)
186
340
  if not in_use:
187
341
  print_flush(f"✅ Port {port} is now free")
@@ -625,7 +779,9 @@ def _start_frontend_npx(frontend_port, backend_port):
625
779
  """Start frontend using published package via npx (production mode)"""
626
780
 
627
781
  # Check if npx is available
628
- npx_path = shutil.which("npx")
782
+ resolver = ExecutableResolver()
783
+ npx_path = resolver.resolve_npx() or shutil.which("npx")
784
+
629
785
  if not npx_path:
630
786
  print_flush("❌ Error: 'npx' command not found!")
631
787
  print_flush(" Please install Node.js and npm first.")
@@ -653,7 +809,7 @@ def _start_frontend_npx(frontend_port, backend_port):
653
809
  if os.name == "nt":
654
810
  env["PYTHONLEGACYWINDOWSSTDIO"] = "0"
655
811
 
656
- npx_cmd = shutil.which("npx") or "npx"
812
+ npx_cmd = npx_path or "npx"
657
813
 
658
814
  # Process group configuration
659
815
  popen_kwargs = {