realtimex-deeptutor 0.5.0.post2__py3-none-any.whl → 0.5.0.post3__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.
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Semi-automatic sync of prompt structure from prompts/en to prompts/zh|prompts/cn.
6
+
7
+ Behavior (safe by default):
8
+ - Dry-run: prints what would be added
9
+ - With --write: adds missing keys to zh/cn files without overwriting existing values
10
+ - With --create-missing-files: creates missing zh/cn files using the en structure
11
+
12
+ NOTE: This tool does NOT translate. It inserts TODO markers to be manually rewritten in Chinese.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ from pathlib import Path
19
+ import sys
20
+ from typing import Any
21
+
22
+ import yaml
23
+
24
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
25
+ AGENTS_DIR = PROJECT_ROOT / "src" / "agents"
26
+
27
+
28
+ def _load_yaml(path: Path) -> Any:
29
+ with open(path, encoding="utf-8") as f:
30
+ return yaml.safe_load(f) or {}
31
+
32
+
33
+ def _dump_yaml(path: Path, obj: Any) -> None:
34
+ path.parent.mkdir(parents=True, exist_ok=True)
35
+ with open(path, "w", encoding="utf-8") as f:
36
+ yaml.safe_dump(obj, f, allow_unicode=True, sort_keys=False)
37
+
38
+
39
+ def _merge_missing(en_obj: Any, zh_obj: Any) -> tuple[Any, int]:
40
+ """
41
+ Add missing keys from en_obj into zh_obj without overwriting existing zh content.
42
+ Returns (new_obj, added_count).
43
+ """
44
+ added = 0
45
+
46
+ if isinstance(en_obj, dict):
47
+ if not isinstance(zh_obj, dict):
48
+ zh_obj = {}
49
+ for k, v in en_obj.items():
50
+ if k not in zh_obj:
51
+ added += 1
52
+ if isinstance(v, str):
53
+ zh_obj[k] = f"<<TODO_TRANSLATE>> {v}"
54
+ else:
55
+ # For non-string nodes, insert scaffold recursively
56
+ zh_obj[k], inc = _merge_missing(
57
+ v, {} if isinstance(v, dict) else [] if isinstance(v, list) else None
58
+ )
59
+ added += inc
60
+ else:
61
+ zh_obj[k], inc = _merge_missing(v, zh_obj[k])
62
+ added += inc
63
+ return zh_obj, added
64
+
65
+ if isinstance(en_obj, list):
66
+ # Do not attempt to merge list structures; keep existing zh list.
67
+ return zh_obj, 0
68
+
69
+ # Primitive leaf: nothing to do
70
+ return zh_obj, 0
71
+
72
+
73
+ def main() -> int:
74
+ parser = argparse.ArgumentParser()
75
+ parser.add_argument("--write", action="store_true", help="write changes to disk")
76
+ parser.add_argument(
77
+ "--create-missing-files",
78
+ action="store_true",
79
+ help="create missing zh/cn files from en structure with TODO markers",
80
+ )
81
+ parser.add_argument(
82
+ "--target",
83
+ choices=["zh", "cn", "both"],
84
+ default="both",
85
+ help="which target language directory to sync",
86
+ )
87
+ args = parser.parse_args()
88
+
89
+ if not AGENTS_DIR.exists():
90
+ print(f"Agents directory not found: {AGENTS_DIR}", file=sys.stderr)
91
+ return 2
92
+
93
+ total_added = 0
94
+ total_files = 0
95
+
96
+ for module_dir in sorted([p for p in AGENTS_DIR.iterdir() if p.is_dir()]):
97
+ prompts_dir = module_dir / "prompts"
98
+ en_dir = prompts_dir / "en"
99
+ if not en_dir.exists():
100
+ continue
101
+
102
+ targets: list[tuple[str, Path]] = []
103
+ if args.target in ("zh", "both"):
104
+ targets.append(("zh", prompts_dir / "zh"))
105
+ if args.target in ("cn", "both"):
106
+ targets.append(("cn", prompts_dir / "cn"))
107
+
108
+ en_files = [p for p in en_dir.rglob("*.yaml") if p.is_file()]
109
+ for en_file in sorted(en_files):
110
+ rel = en_file.relative_to(en_dir)
111
+ en_obj = _load_yaml(en_file)
112
+ for lang_name, lang_dir in targets:
113
+ zh_file = lang_dir / rel
114
+ if not zh_file.exists():
115
+ if not args.create_missing_files:
116
+ print(f"[MISSING {lang_name}] {module_dir.name}: {rel.as_posix()}")
117
+ continue
118
+ zh_obj = {}
119
+ else:
120
+ zh_obj = _load_yaml(zh_file)
121
+
122
+ new_obj, added = _merge_missing(en_obj, zh_obj)
123
+ if added == 0:
124
+ continue
125
+
126
+ total_added += added
127
+ total_files += 1
128
+ print(f"[SYNC {lang_name}] {module_dir.name}: {rel.as_posix()} (+{added} keys)")
129
+
130
+ if args.write:
131
+ _dump_yaml(zh_file, new_obj)
132
+
133
+ if total_files == 0:
134
+ print("No changes needed.")
135
+ return 0
136
+
137
+ if args.write:
138
+ print(f"Updated {total_files} file(s), added {total_added} key(s).")
139
+ else:
140
+ print(
141
+ f"Dry-run: would update {total_files} file(s), add {total_added} key(s). Use --write to apply."
142
+ )
143
+ return 0
144
+
145
+
146
+ if __name__ == "__main__":
147
+ raise SystemExit(main())
src/cli/start.py CHANGED
@@ -15,15 +15,15 @@ Usage:
15
15
 
16
16
  import argparse
17
17
  import os
18
+ from pathlib import Path
18
19
  import subprocess
19
20
  import sys
20
- from pathlib import Path
21
21
 
22
22
 
23
23
  def create_parser() -> argparse.ArgumentParser:
24
24
  """
25
25
  Create CLI argument parser following Unix conventions.
26
-
26
+
27
27
  Design: Minimal CLI options, environment-driven configuration.
28
28
  """
29
29
  parser = argparse.ArgumentParser(
@@ -46,79 +46,71 @@ Environment Variables:
46
46
  For more information: https://github.com/therealtimex/DeepTutor-local-app
47
47
  """,
48
48
  )
49
-
49
+
50
50
  # Mode selection (future extensibility)
51
51
  mode_group = parser.add_mutually_exclusive_group()
52
52
  mode_group.add_argument(
53
53
  "--backend-only",
54
54
  action="store_true",
55
- help="Start backend only (FastAPI) [NOT YET IMPLEMENTED]"
55
+ help="Start backend only (FastAPI) [NOT YET IMPLEMENTED]",
56
56
  )
57
57
  mode_group.add_argument(
58
58
  "--frontend-only",
59
59
  action="store_true",
60
- help="Start frontend only (Next.js) [NOT YET IMPLEMENTED]"
60
+ help="Start frontend only (Next.js) [NOT YET IMPLEMENTED]",
61
61
  )
62
-
62
+
63
63
  # Logging configuration
64
64
  parser.add_argument(
65
65
  "--log-level",
66
66
  choices=["DEBUG", "INFO", "WARNING", "ERROR"],
67
67
  default=os.getenv("LOG_LEVEL", "INFO"),
68
- help="Logging level (default: INFO)"
68
+ help="Logging level (default: INFO)",
69
69
  )
70
-
70
+
71
71
  return parser
72
72
 
73
73
 
74
74
  def find_project_root() -> Path:
75
75
  """
76
76
  Find DeepTutor project root directory.
77
-
77
+
78
78
  Strategy:
79
- 1. Try installed package location
80
- 2. Fall back to development mode (current directory traversal)
81
- 3. Validate by checking for pyproject.toml
82
-
79
+ 1. Use __file__ location (works for both installed and development)
80
+ 2. Fall back to pyproject.toml search for development mode
81
+
83
82
  Returns:
84
83
  Path: Absolute path to project root
85
-
84
+
86
85
  Raises:
87
86
  RuntimeError: If project root cannot be determined
88
87
  """
89
- # Try installed package
90
- try:
91
- import realtimex_deeptutor
92
- root = Path(realtimex_deeptutor.__file__).parent.parent
93
- if (root / "pyproject.toml").exists():
94
- return root
95
- except ImportError:
96
- pass
97
-
98
- # Development mode: traverse up from this file
99
- current = Path(__file__).resolve()
100
- for parent in [current.parent, *current.parents]:
88
+ # Primary strategy: Use __file__ location
89
+ # This file is at: <root>/src/cli/start.py
90
+ # So root is 3 levels up: start.py -> cli -> src -> root
91
+ package_root = Path(__file__).resolve().parent.parent.parent
92
+
93
+ # Validate by checking for essential package content (not pyproject.toml)
94
+ if (package_root / "src" / "api").is_dir():
95
+ return package_root
96
+
97
+ # Fallback: Development mode - look for pyproject.toml
98
+ for parent in Path(__file__).resolve().parents:
101
99
  if (parent / "pyproject.toml").exists():
102
100
  return parent
103
-
104
- # Last resort: current working directory
105
- cwd = Path.cwd()
106
- if (cwd / "pyproject.toml").exists():
107
- return cwd
108
-
109
- raise RuntimeError(
110
- "Cannot find DeepTutor project root. "
111
- "Make sure you are running from the project directory or have installed the package."
112
- )
101
+
102
+ # Last fallback: return computed root without strict validation
103
+ # This allows the package to attempt startup even if structure is unusual
104
+ return package_root
113
105
 
114
106
 
115
107
  def validate_environment(project_root: Path) -> None:
116
108
  """
117
109
  Validate environment and dependencies.
118
-
110
+
119
111
  Args:
120
112
  project_root: Path to project root
121
-
113
+
122
114
  Raises:
123
115
  RuntimeError: If validation fails
124
116
  """
@@ -130,20 +122,24 @@ def validate_environment(project_root: Path) -> None:
130
122
  f"Project root: {project_root}\n"
131
123
  "Please ensure DeepTutor is properly installed."
132
124
  )
133
-
134
- # Check for web directory (frontend)
135
- web_dir = project_root / "web"
136
- if not web_dir.exists():
137
- raise RuntimeError(
138
- f"Frontend directory not found: {web_dir}\n"
139
- "Please ensure the complete DeepTutor package is installed."
140
- )
125
+
126
+ # Check for web directory only in development mode
127
+ # In production mode, frontend is served via npx @realtimex/opentutor-web
128
+ dev_mode = os.environ.get("FRONTEND_DEV_MODE", "").lower() in ("true", "1", "yes")
129
+ if dev_mode:
130
+ web_dir = project_root / "web"
131
+ if not web_dir.exists():
132
+ raise RuntimeError(
133
+ f"Frontend directory not found: {web_dir}\n"
134
+ "FRONTEND_DEV_MODE is enabled but web/ directory is missing.\n"
135
+ "Either disable dev mode or run from the project source directory."
136
+ )
141
137
 
142
138
 
143
139
  def main():
144
140
  """
145
141
  Main CLI entry point.
146
-
142
+
147
143
  Flow:
148
144
  1. Parse arguments
149
145
  2. Find and validate project root
@@ -152,63 +148,59 @@ def main():
152
148
  """
153
149
  parser = create_parser()
154
150
  args = parser.parse_args()
155
-
151
+
156
152
  try:
157
153
  # Find project root
158
154
  project_root = find_project_root()
159
-
155
+
160
156
  # Validate environment
161
157
  validate_environment(project_root)
162
-
158
+
163
159
  # Build environment for subprocess
164
160
  # All configuration comes from environment variables
165
161
  env = os.environ.copy()
166
-
162
+
167
163
  # Set log level
168
164
  env["LOG_LEVEL"] = args.log_level
169
-
165
+
170
166
  # Handle mode selection
171
167
  if args.backend_only:
172
168
  print("⚠️ Backend-only mode not yet implemented")
173
169
  print(" Use 'uvx realtimex-deeptutor' for full-stack startup")
174
170
  print(" Or use 'deeptutor-backend' for backend-only")
175
171
  sys.exit(1)
176
-
172
+
177
173
  if args.frontend_only:
178
174
  print("⚠️ Frontend-only mode not yet implemented")
179
175
  print(" Use 'uvx realtimex-deeptutor' for full-stack startup")
180
176
  print(" Or use 'npx @realtimex/opentutor-web' for frontend-only")
181
177
  sys.exit(1)
182
-
178
+
183
179
  # Full-stack mode: delegate to start_web.py
184
180
  print("🚀 Starting DeepTutor (Full Stack)...")
185
181
  print(f"📁 Project root: {project_root}")
186
-
182
+
187
183
  script_path = project_root / "scripts" / "start_web.py"
188
-
184
+
189
185
  # Launch start_web.py with updated environment
190
- subprocess.run(
191
- [sys.executable, str(script_path)],
192
- env=env,
193
- cwd=project_root,
194
- check=True
195
- )
196
-
186
+ subprocess.run([sys.executable, str(script_path)], env=env, cwd=project_root, check=True)
187
+
197
188
  except KeyboardInterrupt:
198
189
  print("\n🛑 Shutting down...")
199
190
  sys.exit(0)
200
-
191
+
201
192
  except RuntimeError as e:
202
193
  print(f"❌ Error: {e}", file=sys.stderr)
203
194
  sys.exit(1)
204
-
195
+
205
196
  except subprocess.CalledProcessError as e:
206
197
  # start_web.py already printed error messages
207
198
  sys.exit(e.returncode)
208
-
199
+
209
200
  except Exception as e:
210
201
  print(f"❌ Unexpected error: {e}", file=sys.stderr)
211
202
  import traceback
203
+
212
204
  traceback.print_exc()
213
205
  sys.exit(1)
214
206
 
@@ -478,7 +478,7 @@ class UnifiedConfigManager:
478
478
 
479
479
  # Get user's active selection (or use defaults)
480
480
  active = get_rtx_active_config(config_type.value)
481
-
481
+
482
482
  if active:
483
483
  provider = active.get("provider", "realtimexai")
484
484
  model = active.get("model", "")
@@ -596,7 +596,7 @@ class UnifiedConfigManager:
596
596
 
597
597
  if should_use_realtimex_sdk():
598
598
  rtx_active = get_rtx_active_config(config_type.value)
599
-
599
+
600
600
  if rtx_active:
601
601
  return {
602
602
  "id": "rtx",