auto-coder-web 0.1.13__py3-none-any.whl → 0.1.15__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.
@@ -10,6 +10,7 @@ import sys
10
10
  import io
11
11
  import subprocess
12
12
  import byzerllm
13
+ import hashlib
13
14
  from contextlib import contextmanager
14
15
  from byzerllm.utils.langutil import asyncfy_with_semaphore
15
16
  from autocoder.common import AutoCoderArgs
@@ -29,6 +30,7 @@ from autocoder.utils.queue_communicate import (
29
30
  CommunicateEventType,
30
31
  )
31
32
  from autocoder.utils.log_capture import LogCapture
33
+ from autocoder.common import git_utils
32
34
  from threading import Thread
33
35
  from byzerllm.utils import format_str_jinja2
34
36
  from autocoder.index.symbols_utils import (
@@ -37,6 +39,7 @@ from autocoder.index.symbols_utils import (
37
39
  SymbolsInfo,
38
40
  SymbolType,
39
41
  )
42
+ from autocoder.utils.llms import get_single_llm
40
43
 
41
44
 
42
45
  class SymbolItem(BaseModel):
@@ -46,7 +49,7 @@ class SymbolItem(BaseModel):
46
49
 
47
50
 
48
51
  class AutoCoderRunner:
49
- def __init__(self, project_path: str):
52
+ def __init__(self, project_path: str, product_mode: str = "pro"):
50
53
  self.project_path = project_path
51
54
  self.base_persist_dir = os.path.join(
52
55
  project_path, ".auto-coder", "plugins", "chat-auto-coder")
@@ -59,6 +62,8 @@ class AutoCoderRunner:
59
62
  "exclude_dirs": [],
60
63
  }
61
64
  self.load_memory()
65
+ # Configure product mode
66
+ self.configure("product_mode", product_mode)
62
67
 
63
68
  @contextmanager
64
69
  def redirect_stdout(self):
@@ -315,6 +320,82 @@ class AutoCoderRunner:
315
320
  self.save_memory()
316
321
  return {"message": f"Deleted configuration: {key}"}
317
322
 
323
+ def convert_yaml_to_config(self, yaml_file: str):
324
+ """Convert YAML file to AutoCoderArgs configuration"""
325
+ from autocoder.auto_coder import AutoCoderArgs, load_include_files, Template
326
+ args = AutoCoderArgs()
327
+ with open(yaml_file, "r") as f:
328
+ config = yaml.safe_load(f)
329
+ config = load_include_files(config, yaml_file)
330
+ for key, value in config.items():
331
+ if key != "file": # 排除 --file 参数本身
332
+ # key: ENV {{VARIABLE_NAME}}
333
+ if isinstance(value, str) and value.startswith("ENV"):
334
+ template = Template(value.removeprefix("ENV").strip())
335
+ value = template.render(os.environ)
336
+ setattr(args, key, value)
337
+ return args
338
+
339
+ def commit(self):
340
+ """Commit changes using git"""
341
+ try:
342
+ def prepare_commit_yaml():
343
+ auto_coder_main(["next", "chat_action"])
344
+
345
+ prepare_commit_yaml()
346
+
347
+ latest_yaml_file = get_last_yaml_file("actions")
348
+
349
+ if latest_yaml_file:
350
+ try:
351
+ execute_file = os.path.join("actions", latest_yaml_file)
352
+ yaml_config = {
353
+ "include_file": ["./base/base.yml"],
354
+ "auto_merge": self.memory.get("conf", {}).get("auto_merge", "editblock"),
355
+ "human_as_model": self.memory.get("conf", {}).get("human_as_model", "false") == "true",
356
+ "skip_build_index": self.memory.get("conf", {}).get("skip_build_index", "true") == "true",
357
+ "skip_confirm": self.memory.get("conf", {}).get("skip_confirm", "true") == "true",
358
+ "silence": self.memory.get("conf", {}).get("silence", "true") == "true",
359
+ "include_project_structure": self.memory.get("conf", {}).get("include_project_structure", "true") == "true",
360
+ }
361
+
362
+ conf = self.memory.get("conf", {})
363
+ for key, value in conf.items():
364
+ converted_value = self.convert_config_value(key, value)
365
+ if converted_value is not None:
366
+ yaml_config[key] = converted_value
367
+
368
+ yaml_config["urls"] = self.memory["current_files"]["files"] + self.get_llm_friendly_package_docs(
369
+ return_paths=True
370
+ )
371
+ args = self.convert_yaml_to_config(execute_file)
372
+ product_mode = conf.get("product_mode", "pro")
373
+ target_model = args.code_model or args.model
374
+ llm = get_single_llm(target_model, product_mode)
375
+ uncommitted_changes = git_utils.get_uncommitted_changes(".")
376
+ commit_message = git_utils.generate_commit_message.with_llm(
377
+ llm).run(uncommitted_changes)
378
+
379
+ yaml_config["query"] = commit_message
380
+ yaml_content = self.convert_yaml_config_to_str(yaml_config=yaml_config)
381
+ with open(execute_file, "w") as f:
382
+ f.write(yaml_content)
383
+
384
+ file_content = open(execute_file).read()
385
+ md5 = hashlib.md5(file_content.encode('utf-8')).hexdigest()
386
+ file_name = os.path.basename(execute_file)
387
+ commit_result = git_utils.commit_changes(".", f"auto_coder_{file_name}_{md5}")
388
+
389
+ return {"status": True, "message": "Changes committed successfully", "commit_info": commit_result}
390
+ except Exception as e:
391
+ if execute_file:
392
+ os.remove(execute_file)
393
+ return {"status": False, "message": f"Failed to commit: {str(e)}"}
394
+ else:
395
+ return {"status": False, "message": "No changes to commit"}
396
+ except Exception as e:
397
+ return {"status": False, "message": f"Failed to commit: {str(e)}"}
398
+
318
399
  def get_config(self) -> Dict[str, str]:
319
400
  """Get current configuration
320
401
 
auto_coder_web/proxy.py CHANGED
@@ -6,7 +6,6 @@ from fastapi.staticfiles import StaticFiles
6
6
  import uvicorn
7
7
  import httpx
8
8
  import uuid
9
- from typing import Optional, Dict, List, Any
10
9
  import os
11
10
  import argparse
12
11
  import aiofiles
@@ -18,6 +17,21 @@ import sys
18
17
  from .file_group import FileGroupManager
19
18
  from .file_manager import get_directory_tree
20
19
  from .auto_coder_runner import AutoCoderRunner
20
+ from autocoder.agent.auto_filegroup import AutoFileGroup
21
+ from .types import (
22
+ EventGetRequest,
23
+ EventResponseRequest,
24
+ CompletionItem,
25
+ CompletionResponse,
26
+ ChatList,
27
+ HistoryQuery,
28
+ ValidationResponse,
29
+ QueryWithFileNumber,
30
+ ValidationResponseWithFileNumbers,
31
+ FileContentResponse,
32
+ FileChange,
33
+ CommitDiffResponse,
34
+ )
21
35
 
22
36
  from rich.console import Console
23
37
  from prompt_toolkit.shortcuts import radiolist_dialog
@@ -26,7 +40,6 @@ import subprocess
26
40
  from prompt_toolkit import prompt
27
41
  from pydantic import BaseModel
28
42
  from autocoder.utils.log_capture import LogCapture
29
- from typing import Optional, Dict, List, Any
30
43
  from .terminal import terminal_manager
31
44
  from autocoder.common import AutoCoderArgs
32
45
  import json
@@ -35,69 +48,11 @@ import yaml
35
48
  import git
36
49
  import hashlib
37
50
  from datetime import datetime
51
+ from autocoder.utils import operate_config_api
52
+ from .routers import todo_router
53
+ from .routers import settings_router
38
54
 
39
55
 
40
- class EventGetRequest(BaseModel):
41
- request_id: str
42
-
43
-
44
- class EventResponseRequest(BaseModel):
45
- request_id: str
46
- event: Dict[str, str]
47
- response: str
48
-
49
-
50
- class CompletionItem(BaseModel):
51
- name: str
52
- path: str
53
- display: str
54
- location: Optional[str] = None
55
-
56
-
57
- class CompletionResponse(BaseModel):
58
- completions: List[CompletionItem]
59
-
60
-
61
- class ChatList(BaseModel):
62
- name: str
63
- messages: List[Dict[str, Any]]
64
-
65
- class HistoryQuery(BaseModel):
66
- query: str
67
- timestamp: Optional[str] = None
68
-
69
- class ValidationResponse(BaseModel):
70
- success: bool
71
- message: str = ""
72
- queries: List[HistoryQuery] = []
73
-
74
- class QueryWithFileNumber(BaseModel):
75
- query: str
76
- timestamp: Optional[str] = None
77
- file_number: int
78
- response: Optional[str] = None
79
- urls: Optional[List[str]] = None
80
-
81
- class ValidationResponseWithFileNumbers(BaseModel):
82
- success: bool
83
- message: str = ""
84
- queries: List[QueryWithFileNumber] = []
85
-
86
- class FileContentResponse(BaseModel):
87
- success: bool
88
- message: str = ""
89
- content: Optional[str] = None
90
-
91
- class FileChange(BaseModel):
92
- path: str
93
- change_type: str # "added" 或 "modified"
94
-
95
- class CommitDiffResponse(BaseModel):
96
- success: bool
97
- message: str = ""
98
- diff: Optional[str] = None
99
- file_changes: Optional[List[FileChange]] = None
100
-
101
56
  def check_environment():
102
57
  """Check and initialize the required environment"""
103
58
  console = Console()
@@ -114,10 +69,8 @@ def check_environment():
114
69
  console.print(f"✗ {message}", style="red")
115
70
  else:
116
71
  console.print(f" {message}")
117
-
118
- first_time = False
119
- if not os.path.exists("actions") or not os.path.exists(".auto-coder"):
120
- first_time = True
72
+
73
+ if not os.path.exists("actions") or not os.path.exists(".auto-coder"):
121
74
  print_status("Project not initialized", "warning")
122
75
  init_choice = input(
123
76
  " Do you want to initialize the project? (y/n): ").strip().lower()
@@ -271,23 +224,113 @@ def check_environment():
271
224
  return True
272
225
 
273
226
 
227
+ def check_environment_lite():
228
+ """Check and initialize the required environment for lite mode"""
229
+ console = Console()
230
+ console.print("\n[blue]Initializing the environment (Lite Mode)...[/blue]")
231
+
232
+ def check_project():
233
+ """Check if the current directory is initialized as an auto-coder project"""
234
+ def print_status(message, status):
235
+ if status == "success":
236
+ console.print(f"✓ {message}", style="green")
237
+ elif status == "warning":
238
+ console.print(f"! {message}", style="yellow")
239
+ elif status == "error":
240
+ console.print(f"✗ {message}", style="red")
241
+ else:
242
+ console.print(f" {message}")
243
+
244
+ first_time = False
245
+ if not os.path.exists("actions") or not os.path.exists(".auto-coder"):
246
+ first_time = True
247
+ print_status("Project not initialized", "warning")
248
+ init_choice = input(
249
+ " Do you want to initialize the project? (y/n): ").strip().lower()
250
+ if init_choice == "y":
251
+ try:
252
+ if not os.path.exists("actions"):
253
+ os.makedirs("actions", exist_ok=True)
254
+ print_status("Created actions directory", "success")
255
+
256
+ if not os.path.exists(".auto-coder"):
257
+ os.makedirs(".auto-coder", exist_ok=True)
258
+ print_status(
259
+ "Created .auto-coder directory", "success")
260
+
261
+ subprocess.run(
262
+ ["auto-coder", "init", "--source_dir", "."], check=True)
263
+ print_status("Project initialized successfully", "success")
264
+ except subprocess.CalledProcessError:
265
+ print_status("Failed to initialize project", "error")
266
+ print_status(
267
+ "Please try to initialize manually: auto-coder init --source_dir .", "warning")
268
+ return False
269
+ else:
270
+ print_status("Exiting due to no initialization", "warning")
271
+ return False
272
+
273
+ print_status("Project initialization check complete", "success")
274
+ return True
275
+
276
+ if not check_project():
277
+ return False
278
+
279
+ def print_status(message, status):
280
+ if status == "success":
281
+ console.print(f"✓ {message}", style="green")
282
+ elif status == "warning":
283
+ console.print(f"! {message}", style="yellow")
284
+ elif status == "error":
285
+ console.print(f"✗ {message}", style="red")
286
+ else:
287
+ console.print(f" {message}")
288
+
289
+ # Setup deepseek api key
290
+ api_key_dir = os.path.expanduser("~/.auto-coder/keys")
291
+ api_key_file = os.path.join(api_key_dir, "api.deepseek.com")
292
+
293
+ if not os.path.exists(api_key_file):
294
+ print_status("API key not found", "warning")
295
+ api_key = prompt(HTML("<b>Please enter your API key: </b>"))
296
+
297
+ # Create directory if it doesn't exist
298
+ os.makedirs(api_key_dir, exist_ok=True)
299
+
300
+ # Save the API key
301
+ with open(api_key_file, "w") as f:
302
+ f.write(api_key)
303
+
304
+ print_status(f"API key saved successfully: {api_key_file}", "success")
305
+
306
+ print_status("Environment initialization complete", "success")
307
+ return True
308
+
309
+
274
310
  class ProxyServer:
275
- def __init__(self, project_path: str, quick: bool = False):
311
+ def __init__(self, project_path: str, quick: bool = False, product_mode: str = "pro"):
276
312
  self.app = FastAPI()
277
313
 
278
314
  if not quick:
279
- # Check the environment if not in quick mode
280
- if not check_environment():
281
- print(
282
- "\033[31mEnvironment check failed. Some features may not work properly.\033[0m")
315
+ # Check the environment based on product mode
316
+ if product_mode == "lite":
317
+ if not check_environment_lite():
318
+ print(
319
+ "\033[31mEnvironment check failed. Some features may not work properly.\033[0m")
320
+ else:
321
+ if not check_environment():
322
+ print(
323
+ "\033[31mEnvironment check failed. Some features may not work properly.\033[0m")
324
+
283
325
  self.setup_middleware()
284
326
 
285
327
  self.setup_static_files()
286
328
 
287
329
  self.setup_routes()
330
+
288
331
  self.client = httpx.AsyncClient()
289
332
  self.project_path = project_path
290
- self.auto_coder_runner = AutoCoderRunner(project_path)
333
+ self.auto_coder_runner = AutoCoderRunner(project_path, product_mode=product_mode)
291
334
  self.file_group_manager = FileGroupManager(self.auto_coder_runner)
292
335
 
293
336
  def setup_middleware(self):
@@ -308,6 +351,10 @@ class ProxyServer:
308
351
  "/static", StaticFiles(directory=self.static_dir), name="static")
309
352
 
310
353
  def setup_routes(self):
354
+
355
+ self.app.include_router(todo_router.router)
356
+ self.app.include_router(settings_router.router)
357
+
311
358
  @self.app.on_event("shutdown")
312
359
  async def shutdown_event():
313
360
  await self.client.aclose()
@@ -357,6 +404,42 @@ class ProxyServer:
357
404
  group = await self.file_group_manager.create_group(name, description)
358
405
  return group
359
406
 
407
+ @self.app.post("/api/file-groups/auto")
408
+ async def auto_create_groups(request: Request):
409
+ try:
410
+ data = await request.json()
411
+ file_size_limit = data.get("file_size_limit", 100)
412
+ skip_diff = data.get("skip_diff", False)
413
+ group_num_limit = data.get("group_num_limit", 10)
414
+
415
+ # Create AutoFileGroup instance
416
+ auto_grouper = AutoFileGroup(
417
+ operate_config_api.get_llm(self.auto_coder_runner.memory),
418
+ self.project_path,
419
+ skip_diff=skip_diff,
420
+ file_size_limit=file_size_limit,
421
+ group_num_limit=group_num_limit
422
+ )
423
+
424
+ # Get groups
425
+ groups = auto_grouper.group_files()
426
+
427
+ # Create groups using file_group_manager
428
+ for group in groups:
429
+ await self.file_group_manager.create_group(
430
+ name=group.name,
431
+ description=group.description
432
+ )
433
+ # Add files to the group
434
+ await self.file_group_manager.add_files_to_group(
435
+ group.name,
436
+ group.urls
437
+ )
438
+
439
+ return {"status": "success", "message": f"Created {len(groups)} groups"}
440
+ except Exception as e:
441
+ raise HTTPException(status_code=500, detail=str(e))
442
+
360
443
  @self.app.get("/api/os")
361
444
  async def get_os():
362
445
  return {"os": os.name}
@@ -585,6 +668,14 @@ class ProxyServer:
585
668
  request_id, request.event, request.response)
586
669
  return {"message": "success"}
587
670
 
671
+ @self.app.post("/api/commit")
672
+ async def commit():
673
+ try:
674
+ result = self.auto_coder_runner.commit()
675
+ return result
676
+ except Exception as e:
677
+ raise HTTPException(status_code=500, detail=str(e))
678
+
588
679
  @self.app.get("/api/output/{request_id}")
589
680
  async def get_terminal_logs(request_id: str):
590
681
  return self.auto_coder_runner.get_logs(request_id)
@@ -835,6 +926,17 @@ class ProxyServer:
835
926
 
836
927
 
837
928
  def main():
929
+ from autocoder.rag.variable_holder import VariableHolder
930
+ from tokenizers import Tokenizer
931
+ try:
932
+ tokenizer_path = pkg_resources.resource_filename(
933
+ "autocoder", "data/tokenizer.json"
934
+ )
935
+ VariableHolder.TOKENIZER_PATH = tokenizer_path
936
+ VariableHolder.TOKENIZER_MODEL = Tokenizer.from_file(tokenizer_path)
937
+ except FileNotFoundError:
938
+ tokenizer_path = None
939
+
838
940
  parser = argparse.ArgumentParser(description="Proxy Server")
839
941
  parser.add_argument(
840
942
  "--port",
@@ -853,9 +955,31 @@ def main():
853
955
  action="store_true",
854
956
  help="Skip environment check",
855
957
  )
958
+ parser.add_argument(
959
+ "--product_mode",
960
+ type=str,
961
+ default="pro",
962
+ help="The mode of the auto-coder.chat, lite/pro default is pro",
963
+ )
964
+ parser.add_argument(
965
+ "--lite",
966
+ action="store_true",
967
+ help="Run in lite mode (equivalent to --product_mode lite)",
968
+ )
969
+ parser.add_argument(
970
+ "--pro",
971
+ action="store_true",
972
+ help="Run in pro mode (equivalent to --product_mode pro)",
973
+ )
856
974
  args = parser.parse_args()
857
975
 
858
- proxy_server = ProxyServer(quick=args.quick, project_path=os.getcwd())
976
+ # Handle lite/pro flags
977
+ if args.lite:
978
+ args.product_mode = "lite"
979
+ elif args.pro:
980
+ args.product_mode = "pro"
981
+
982
+ proxy_server = ProxyServer(quick=args.quick, project_path=os.getcwd(), product_mode=args.product_mode)
859
983
  uvicorn.run(proxy_server.app, host=args.host, port=args.port)
860
984
 
861
985
 
File without changes
@@ -0,0 +1,76 @@
1
+
2
+ import os
3
+ import json
4
+ import logging
5
+ import aiofiles
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+ from typing import Optional
9
+ from pathlib import Path
10
+ import asyncio
11
+
12
+ router = APIRouter()
13
+
14
+ # 配置存储路径
15
+ SETTINGS_FILE = Path(".auto-coder/auto-coder.web/settings/settings.json")
16
+
17
+ # 确保目录存在
18
+ SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class SettingsModel(BaseModel):
23
+ language: str = "zh"
24
+ theme: str = "dark"
25
+ font_size: int = 14
26
+ auto_save: bool = True
27
+ show_line_numbers: bool = True
28
+
29
+ async def load_settings() -> SettingsModel:
30
+ """异步加载设置"""
31
+ if not await asyncio.to_thread(lambda: SETTINGS_FILE.exists()):
32
+ # 如果文件不存在,返回默认设置
33
+ return SettingsModel()
34
+
35
+ try:
36
+ async with aiofiles.open(SETTINGS_FILE, mode='r') as f:
37
+ content = await f.read()
38
+ return SettingsModel(**json.loads(content))
39
+ except (json.JSONDecodeError, FileNotFoundError):
40
+ logger.error("Failed to parse settings.json, returning default settings")
41
+ return SettingsModel()
42
+
43
+ async def save_settings(settings: SettingsModel):
44
+ """异步保存设置"""
45
+ async with aiofiles.open(SETTINGS_FILE, mode='w') as f:
46
+ await f.write(settings.json(indent=2, ensure_ascii=False))
47
+
48
+ @router.get("/api/settings")
49
+ async def get_settings():
50
+ """获取当前设置"""
51
+ return await load_settings()
52
+
53
+ @router.post("/api/settings")
54
+ async def update_settings(settings: dict):
55
+ """更新设置"""
56
+ current_settings = await load_settings()
57
+ updated_settings = current_settings.copy(update=settings)
58
+ await save_settings(updated_settings)
59
+ return updated_settings
60
+
61
+ @router.post("/api/settings/language")
62
+ async def set_language(language: str):
63
+ """设置语言"""
64
+ if language not in ["zh", "en"]:
65
+ raise HTTPException(status_code=400, detail="Invalid language, must be 'zh' or 'en'")
66
+
67
+ current_settings = await load_settings()
68
+ current_settings.language = language
69
+ await save_settings(current_settings)
70
+ return {"status": "success", "language": language}
71
+
72
+ @router.get("/api/settings/language")
73
+ async def get_language():
74
+ """获取当前语言设置"""
75
+ settings = await load_settings()
76
+ return {"language": settings.language}