├── src
└── readmex
│ ├── __init__.py
│ ├── utils
│ ├── __init__.py
│ ├── logo_generator.py
│ ├── file_handler.py
│ ├── cli.py
│ ├── language_analyzer.py
│ ├── dependency_analyzer.py
│ └── model_client.py
│ ├── images
│ ├── logo.png
│ └── screenshot.png
│ ├── __main__.py
│ ├── config
│ ├── language_mapping.json
│ ├── ignore_patterns.json
│ └── dependency_config.json
│ ├── templates
│ └── BLANK_README.md
│ └── config.py
├── tests
├── test_core_basic_info.py
├── test_llm.py
├── test_generate.py
├── test_error_handling.py
├── test_image_generation.py
├── test_t2i_config.py
├── test_azure_openai_simple.py
├── test_auto_description.py
├── test_core_threading.py
├── test_extended_auto_generation.py
├── test_logo_generator.py
└── test_complete_dependency_analysis.py
├── images
├── flow.png
├── logo.png
├── poster.png
└── group_qr.png
├── requirements.txt
├── source.env
├── config_template.json
├── .github
└── workflows
│ └── publish.yml
├── LICENSE.txt
├── pyproject.toml
├── .gitignore
├── .flake8
├── README_CN.md
└── README.md
/src/readmex/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/readmex/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_core_basic_info.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/flow.png
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/logo.png
--------------------------------------------------------------------------------
/images/poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/poster.png
--------------------------------------------------------------------------------
/images/group_qr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/images/group_qr.png
--------------------------------------------------------------------------------
/src/readmex/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/src/readmex/images/logo.png
--------------------------------------------------------------------------------
/src/readmex/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aibox22/readmeX/HEAD/src/readmex/images/screenshot.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | python-dotenv>=0.15.0
2 | numpy>=1.21.0
3 | openai>=0.11.0
4 | rich>=10.0.0
5 | requests>=2.25.0
6 | pytest>=6.0.0
7 | flake8>=7.0.0
--------------------------------------------------------------------------------
/src/readmex/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | readmex package main entry point.
4 | Allows running the package as a module with: python -m readmex
5 | """
6 |
7 | from readmex.utils.cli import main
8 |
9 | if __name__ == '__main__':
10 | main()
--------------------------------------------------------------------------------
/source.env:
--------------------------------------------------------------------------------
1 | # LLM 配置
2 | LLM_BASE_URL=https://api.deepbricks.ai/v1/
3 | LLM_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0
4 | LLM_MODEL_NAME=gpt-4o
5 |
6 | # 文生图配置
7 | T2I_BASE_URL=https://api.deepbricks.ai/v1/
8 | T2I_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0
9 | T2I_MODEL_NAME=dall-e-3
10 |
11 | # Embedding 配置
12 | EMBEDDING_BASE_URL=https://api.deepbricks.ai/v1/
13 | EMBEDDING_API_KEY=sk-upX1ZDkrORvWcQjdnB8h2CdrjLIPcsqqHqOl7titUUE7p5o0
14 | EMBEDDING_MODEL_NAME=text-embedding-3-small
15 | LOCAL_EMBEDDING=false
16 |
--------------------------------------------------------------------------------
/config_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "LLM_API_KEY": "",
3 | "LLM_BASE_URL": "https://api.openai.com/v1",
4 | "LLM_MODEL_NAME": "gpt-3.5-turbo",
5 | "T2I_API_KEY": "",
6 | "T2I_BASE_URL": "https://api.openai.com/v1",
7 | "T2I_MODEL_NAME": "dall-e-3",
8 | "EMBEDDING_API_KEY": "",
9 | "EMBEDDING_BASE_URL": "https://api.openai.com/v1",
10 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small",
11 | "LOCAL_EMBEDDING": "false",
12 | "MAX_WORKERS": "10",
13 | "GITHUB_USERNAME": "",
14 | "TWITTER_HANDLE": "",
15 | "LINKEDIN_USERNAME": "",
16 | "EMAIL": ""
17 | }
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package to PyPI
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 |
8 | jobs:
9 | build-and-publish:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v4
18 | with:
19 | python-version: '3.9'
20 |
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install build twine
25 |
26 | - name: Build package
27 | run: python -m build
28 |
29 | - name: Publish package
30 | run: twine upload --repository pypi --skip-existing dist/* --verbose
31 | env:
32 | TWINE_USERNAME: __token__
33 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
34 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 lintaojlu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tests/test_llm.py:
--------------------------------------------------------------------------------
1 | # tests/test_model_client.py
2 | # 测试 ModelClient 类的 get_answer 和 get_image 方法
3 |
4 | import pytest
5 | from pathlib import Path
6 | import sys
7 | root_dir = Path(__file__).parent.parent
8 | sys.path.append(str(root_dir))
9 | from src.readmex.utils.model_client import ModelClient
10 |
11 | # 测试 get_answer 方法
12 | def test_get_answer():
13 | # 实例化 ModelClient
14 | client = ModelClient()
15 | # 构造问题
16 | question = "你好,介绍一下你自己"
17 | # 调用 get_answer 获取回复
18 | answer = client.get_answer(question)
19 | print("get_answer 返回:", answer)
20 | # 断言返回内容不为空
21 | assert answer is not None and len(answer) > 0
22 |
23 | # 测试 get_image 方法
24 | # @pytest.mark.skip(reason="生成图片会消耗额度,调试时可去掉本行")
25 | def test_get_image():
26 | # 实例化 ModelClient
27 | client = ModelClient()
28 | # 设置图片生成的 prompt
29 | prompt = "一只可爱的卡通猫,蓝色背景"
30 | # 调用 get_image 获取图片内容
31 | img_result = client.get_image(prompt)
32 | print("get_image 返回结果:", img_result)
33 | # 断言返回结果包含url或content
34 | assert img_result is not None
35 | assert "url" in img_result or "content" in img_result
36 |
37 | # 测试配置信息获取
38 | def test_get_current_settings():
39 | # 实例化 ModelClient
40 | client = ModelClient()
41 | # 获取当前设置
42 | settings = client.get_current_settings()
43 | print("当前设置:", settings)
44 | # 断言设置不为空
45 | assert settings is not None
46 | assert "llm_model_name" in settings
47 | assert "t2i_model_name" in settings
--------------------------------------------------------------------------------
/tests/test_generate.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script to verify the generate method works with project_path parameter
4 | and configuration loading logic.
5 | """
6 |
7 | import sys
8 | import os
9 | sys.path.insert(0, 'src')
10 |
11 | from readmex.core import readmex
12 | from rich.console import Console
13 |
14 | def test_generate():
15 | console = Console()
16 | console.print("[bold cyan]Testing readmex generate method...[/bold cyan]")
17 |
18 | try:
19 | # Create generator instance
20 | generator = readmex()
21 |
22 | # Test with current directory
23 | current_dir = os.getcwd()
24 | console.print(f"[green]Testing with project path: {current_dir}[/green]")
25 |
26 | # This should now work without the "takes 1 positional argument but 2 were given" error
27 | generator.generate(current_dir)
28 |
29 | console.print("[bold green]✅ Test passed! Generate method accepts project_path parameter.[/bold green]")
30 |
31 | except TypeError as e:
32 | if "takes 1 positional argument but 2 were given" in str(e):
33 | console.print("[bold red]❌ Test failed! Generate method still has parameter issue.[/bold red]")
34 | console.print(f"[red]Error: {e}[/red]")
35 | else:
36 | console.print(f"[yellow]Different TypeError: {e}[/yellow]")
37 | except Exception as e:
38 | console.print(f"[yellow]Other error (expected during testing): {e}[/yellow]")
39 | console.print("[green]✅ Parameter issue is fixed, but other configuration issues exist.[/green]")
40 |
41 | if __name__ == "__main__":
42 | test_generate()
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=45.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "readmex"
7 | version = "0.3.0"
8 | authors = [
9 | { name="stone91", email="m370025263@gmail.com" },
10 | { name="lintao", email="lint22@mails.tsinghua.edu.cn"},
11 | ]
12 | description = "A tool to automatically generate README files for your projects."
13 | readme = "README.md"
14 | requires-python = ">=3.8"
15 | classifiers = [
16 | "Programming Language :: Python :: 3",
17 | "License :: OSI Approved :: MIT License",
18 | "Operating System :: OS Independent",
19 | ]
20 | dependencies = [
21 | "python-dotenv>=0.15.0",
22 | "numpy>=1.21.0",
23 | "openai>=0.11.0",
24 | "rich>=10.0.0",
25 | "requests>=2.25.0",
26 | "pytest>=6.0.0",
27 | "sentence-transformers>=2.0.0",
28 | "faiss-cpu>=1.7.0",
29 | "flake8>=7.0.0"
30 | ]
31 |
32 | [tool.setuptools.packages.find]
33 | where = ["src"]
34 |
35 | [tool.setuptools.package-data]
36 | readmex = ["**/*.md", "**/*.json", "**/*.png"]
37 |
38 | [project.scripts]
39 | readmex = "readmex.utils.cli:main"
40 |
41 | [project.urls]
42 | "Homepage" = "https://github.com/aibox22/readmex"
43 | "Bug Tracker" = "https://github.com/aibox22/readmex/issues"
44 |
45 | [tool.flake8]
46 | max-line-length = 120
47 | ignore = ["E501", "W503", "F401", "E402", "F841", "E203", "W504", "W293", "E301", "E302", "W291", "W292", "F541", "F811", "E305", "E128", "E127", "E129", "E226", "E722", "E303", "E261", "F824", "E114", "E116", "F403", "F405", "E731", "E704", "W601", "W602", "W603", "W604"]
48 | exclude = [".git", "__pycache__", ".venv", ".env", "build", "dist", "*.egg-info"]
49 | per-file-ignores = [
50 | "tests/*.py:F401,E402,F841,E501,W293,E301,E302,W291,W292,F541,E305",
51 | "__init__.py:F401"
52 | ]
53 |
--------------------------------------------------------------------------------
/src/readmex/config/language_mapping.json:
--------------------------------------------------------------------------------
1 | {
2 | "python": [".py", ".pyx", ".pyi", ".pyw", ".ipynb"],
3 | "javascript": [".js", ".jsx", ".mjs"],
4 | "typescript": [".ts", ".tsx"],
5 | "java": [".java", ".jar", ".class"],
6 | "c": [".c", ".o", ".so", ".dll", ".dylib"],
7 | "cpp": [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h++", ".o", ".so", ".dll", ".dylib"],
8 | "csharp": [".cs", ".dll", ".exe"],
9 | "go": [".go", ".exe"],
10 | "rust": [".rs", ".exe", ".dll", ".so", ".dylib"],
11 | "php": [".php", ".phtml"],
12 | "ruby": [".rb", ".erb", ".rake"],
13 | "swift": [".swift"],
14 | "kotlin": [".kt", ".kts"],
15 | "scala": [".scala"],
16 | "html": [".html", ".htm"],
17 | "css": [".css"],
18 | "scss": [".scss"],
19 | "sass": [".sass"],
20 | "less": [".less"],
21 | "stylus": [".styl"],
22 | "pug": [".pug", ".jade"],
23 | "handlebars": [".hbs", ".handlebars"],
24 | "mustache": [".mustache"],
25 | "twig": [".twig"],
26 | "smarty": [".tpl"],
27 | "jinja": [".j2", ".jinja2"],
28 | "vue": [".vue"],
29 | "r": [".r", ".R"],
30 | "matlab": [".m", ".mat"],
31 | "perl": [".pl", ".pm"],
32 | "lua": [".lua"],
33 | "dart": [".dart"],
34 | "fsharp": [".fs"],
35 | "visualbasic": [".vb"],
36 | "assembly": [".asm", ".s", ".S"],
37 | "objectivec": [".m", ".mm"],
38 | "haskell": [".hs"],
39 | "erlang": [".erl"],
40 | "elixir": [".ex", ".exs"],
41 | "clojure": [".clj", ".cljs"],
42 | "coffeescript": [".coffee"],
43 | "powershell": [".ps1", ".psm1"],
44 | "shell": [".sh", ".bash", ".zsh", ".fish"],
45 | "batch": [".bat", ".cmd"],
46 | "solidity": [".sol"],
47 | "nix": [".nix"],
48 | "terraform": [".tf", ".tfvars"],
49 | "dockerfile": ["Dockerfile", ".dockerfile"],
50 | "makefile": ["Makefile", "makefile", ".mk"],
51 | "cmake": [".cmake", "CMakeLists.txt"],
52 | "gradle": [".gradle"],
53 | "maven": [".pom"],
54 | "jupyter": [".ipynb"],
55 | "vim": [".vim"],
56 | "emacs": [".el"],
57 | "protobuf": [".proto"],
58 | "graphql": [".graphql", ".gql"],
59 | "webassembly": [".wasm", ".wat"]
60 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | website/
13 | tests/
14 | .rag_cache/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | -pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | .pytest_cache/
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 | db.sqlite3
57 |
58 | # Flask stuff:
59 | instance/
60 | .webassets-cache
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # Jupyter Notebook
72 | .ipynb_checkpoints
73 |
74 | # IPython
75 | profile_default/
76 | ipython_config.py
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # PEP 582; used by Poetry, pdm and others
82 | __pypackages__/
83 |
84 | # Celery stuff
85 | celerybeat-schedule
86 | celerybeat.pid
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | .env
98 | env.bak/
99 | venv.bak/
100 |
101 | # Spyder project settings
102 | .spyderproject
103 | .spyproject
104 |
105 | # Rope project settings
106 | .ropeproject
107 |
108 | # mkdocs documentation
109 | /site
110 |
111 | # mypy
112 | .mypy_cache/
113 | .dmypy.json
114 | dmypy.json
115 |
116 | # Pyre type checker
117 | .pyre/
118 |
119 | # macOS system files
120 | .DS_Store
121 | .DS_Store?
122 | ._*
123 | .Spotlight-V100
124 | .Trashes
125 | ehthumbs.db
126 | Thumbs.db
127 |
128 | # readmex_output
129 | *output*/
130 | # PyCharm/IntelliJ IDEA
131 | .idea/
132 | .vscode/launch.json
133 |
134 | # Local configurations
135 | .readmex
--------------------------------------------------------------------------------
/tests/test_error_handling.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script to verify improved error handling and configuration display
4 | """
5 |
6 | import sys
7 | import os
8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
9 |
10 | from rich.console import Console
11 | from readmex.config import load_config, get_config_sources
12 | from readmex.utils.cli import main
13 |
14 | def test_error_handling():
15 | """Test the improved error handling with configuration display"""
16 | console = Console()
17 |
18 | try:
19 | # Simulate a connection error
20 | raise ConnectionError("Connection error")
21 |
22 | except Exception as e:
23 | console.print(f"[bold red]An error occurred: {e}[/bold red]")
24 |
25 | # Show configuration information to help with debugging
26 | try:
27 | config = load_config()
28 | sources = get_config_sources()
29 | if config and sources:
30 | # Show configuration source info once
31 | console.print("\n[yellow]Configuration loaded from:[/yellow]")
32 | source_files = set(sources.values())
33 | for source_file in source_files:
34 | if "Environment Variable" not in source_file:
35 | console.print(f"[yellow] • {source_file}[/yellow]")
36 |
37 | # Show configuration table with actual values
38 | from rich.table import Table
39 | table = Table(title="[bold cyan]Current Configuration[/bold cyan]")
40 | table.add_column("Variable", style="cyan")
41 | table.add_column("Value", style="green")
42 |
43 | # Only show non-sensitive configuration values
44 | display_keys = ["llm_model_name", "t2i_model_name", "llm_base_url", "t2i_base_url",
45 | "github_username", "twitter_handle", "linkedin_username", "email"]
46 |
47 | for key in display_keys:
48 | if key in config and config[key]:
49 | value = config[key]
50 | # Mask API keys for security
51 | if "api_key" in key.lower():
52 | value = "***" + value[-4:] if len(value) > 4 else "***"
53 | table.add_row(key, value)
54 |
55 | console.print(table)
56 | except Exception as config_error:
57 | console.print(f"[red]Could not load configuration: {config_error}[/red]")
58 |
59 | if __name__ == "__main__":
60 | test_error_handling()
--------------------------------------------------------------------------------
/tests/test_image_generation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script to verify fixed image generation functionality
4 | """
5 |
6 | import sys
7 | import os
8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
9 |
10 | from rich.console import Console
11 | from readmex.utils.model_client import ModelClient
12 |
13 | def test_image_generation():
14 | """Test the fixed image generation functionality"""
15 | console = Console()
16 |
17 | console.print("[bold cyan]=== Testing Fixed Image Generation ===[/bold cyan]")
18 |
19 | try:
20 | # Create model client
21 | client = ModelClient()
22 |
23 | # Display current settings
24 | console.print("\n[yellow]Current Settings:[/yellow]")
25 | settings = client.get_current_settings()
26 | for key, value in settings.items():
27 | console.print(f" {key}: {value}")
28 |
29 | # Test image generation with a simple prompt
30 | console.print("\n[yellow]Testing image generation...[/yellow]")
31 | prompt = "A simple geometric logo with blue and white colors"
32 |
33 | console.print(f"[cyan]Prompt: {prompt}[/cyan]")
34 |
35 | # Generate image
36 | result = client.get_image(prompt)
37 |
38 | if result and "url" in result:
39 | console.print("[bold green]✓ Image generation successful![/bold green]")
40 | console.print(f"[green]Image URL: {result['url']}[/green]")
41 | if result.get('content'):
42 | console.print(f"[green]Content size: {len(result['content'])} bytes[/green]")
43 | else:
44 | console.print("[yellow]⚠️ Image URL generated but content download failed[/yellow]")
45 | else:
46 | console.print("[red]✗ Image generation failed[/red]")
47 |
48 | except Exception as e:
49 | console.print(f"[red]✗ Test failed with error: {e}[/red]")
50 |
51 | # Check if it's the specific model parameter error
52 | if "InvalidParameter" in str(e) and "model" in str(e):
53 | console.print("\n[yellow]This appears to be a model parameter issue.[/yellow]")
54 | console.print("[yellow]The fix should have resolved this. Please check:[/yellow]")
55 | console.print("1. Model name compatibility with your API provider")
56 | console.print("2. API endpoint configuration")
57 | console.print("3. API key validity")
58 |
59 | return False
60 |
61 | return True
62 |
63 | if __name__ == "__main__":
64 | success = test_image_generation()
65 | if success:
66 | print("\n🎉 Image generation test completed successfully!")
67 | else:
68 | print("\n❌ Image generation test failed.")
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | # Maximum line length
3 | max-line-length = 120
4 |
5 | # Ignore specific error codes
6 | ignore =
7 | # E501: Line too long (handled by max-line-length)
8 | E501,
9 | # W503: Line break before binary operator (conflicts with W504)
10 | W503,
11 | # F401: Module imported but unused (common in test files)
12 | F401,
13 | # E402: Module level import not at top of file (needed for sys.path manipulation)
14 | E402,
15 | # F841: Local variable assigned but never used (common in tests)
16 | F841,
17 | # E203: Whitespace before ':' (conflicts with black formatter)
18 | E203,
19 | # W504: Line break after binary operator
20 | W504,
21 | # W293: Blank line contains whitespace
22 | W293,
23 | # E301: Expected 1 blank line, found 0
24 | E301,
25 | # E302: Expected 2 blank lines, found 1
26 | E302,
27 | # W291: Trailing whitespace (common formatting issue)
28 | W291,
29 | # W292: No newline at end of file
30 | W292,
31 | # F541: f-string is missing placeholders (often used for consistency)
32 | F541,
33 | # F811: Redefinition of unused import (common with conditional imports)
34 | F811,
35 | # E305: Expected 2 blank lines after class or function definition, found 1
36 | E305,
37 | # E128: Continuation line under-indented for visual indent
38 | E128,
39 | # E127: Continuation line over-indented for visual indent
40 | E127,
41 | # E129: Visually indented line with same indent as next logical line
42 | E129,
43 | # E226: Missing whitespace around arithmetic operator
44 | E226,
45 | # E722: Do not use bare 'except' (sometimes necessary for broad exception handling)
46 | E722,
47 | # E303: Too many blank lines
48 | E303,
49 | # E261: At least two spaces before inline comment
50 | E261,
51 | # F824: Global variable is unused
52 | F824,
53 | # E114: Indentation is not a multiple of 4 (comment)
54 | E114,
55 | # E116: Unexpected indentation (comment)
56 | E116,
57 | # F403: 'from module import *' used; unable to detect undefined names
58 | F403,
59 | # F405: Name may be undefined, or defined from star imports
60 | F405,
61 | # E731: Do not assign a lambda expression, use a def
62 | E731,
63 | # E704: Multiple statements on one line (def)
64 | E704,
65 | # W601: .has_key() is deprecated, use 'in'
66 | W601,
67 | # W602: Deprecated form of raising exception
68 | W602,
69 | # W603: '<>' is deprecated, use '!='
70 | W603,
71 | # W604: Backticks are deprecated, use 'repr()'
72 | W604
73 |
74 | # Exclude directories and files
75 | exclude =
76 | .git,
77 | __pycache__,
78 | .venv,
79 | .env,
80 | build,
81 | dist,
82 | *.egg-info,
83 | .tox,
84 | .coverage,
85 | htmlcov,
86 | .pytest_cache
87 |
88 | # Per-file ignores for test files
89 | per-file-ignores =
90 | # Test files can have additional relaxed rules
91 | tests/*.py:F401,E402,F841,E501
92 | # Init files often have unused imports
93 | __init__.py:F401
94 |
--------------------------------------------------------------------------------
/tests/test_t2i_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script to check T2I configuration and model compatibility
4 | """
5 |
6 | import sys
7 | import os
8 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
9 |
10 | from rich.console import Console
11 | from readmex.config import load_config, get_t2i_config
12 | from readmex.utils.model_client import ModelClient
13 |
14 | def test_t2i_config():
15 | """Test T2I configuration and model compatibility"""
16 | console = Console()
17 |
18 | console.print("[bold cyan]=== T2I Configuration Check ===[/bold cyan]")
19 |
20 | # Load and display configuration
21 | config = load_config()
22 | t2i_config = get_t2i_config()
23 |
24 | console.print("\n[yellow]Current T2I Configuration:[/yellow]")
25 | for key, value in t2i_config.items():
26 | if "api_key" in key:
27 | masked_value = "***" + value[-4:] if value and len(value) > 4 else "***"
28 | console.print(f" {key}: {masked_value}")
29 | else:
30 | console.print(f" {key}: {value}")
31 |
32 | # Check model compatibility
33 | model_name = t2i_config.get("model_name", "dall-e-3")
34 | base_url = t2i_config.get("base_url", "https://api.openai.com/v1")
35 |
36 | console.print(f"\n[yellow]Model Analysis:[/yellow]")
37 | console.print(f" Model Name: {model_name}")
38 | console.print(f" Base URL: {base_url}")
39 |
40 | # Check if using custom API endpoint
41 | if "openai.com" not in base_url:
42 | console.print(f"\n[yellow]⚠️ Using custom API endpoint: {base_url}[/yellow]")
43 | console.print("[yellow]Common model compatibility issues:[/yellow]")
44 | console.print(" • Some providers may not support 'dall-e-3' model name")
45 | console.print(" • Try alternative model names like:")
46 | console.print(" - 'dall-e'")
47 | console.print(" - 'text-to-image'")
48 | console.print(" - 'stable-diffusion'")
49 | console.print(" - Check your provider's documentation for supported models")
50 |
51 | # Test model client initialization
52 | console.print("\n[yellow]Testing ModelClient initialization:[/yellow]")
53 | try:
54 | client = ModelClient()
55 | console.print("[green]✓ ModelClient initialized successfully[/green]")
56 |
57 | # Display current settings
58 | settings = client.get_current_settings()
59 | console.print("\n[yellow]ModelClient Settings:[/yellow]")
60 | for key, value in settings.items():
61 | console.print(f" {key}: {value}")
62 |
63 | except Exception as e:
64 | console.print(f"[red]✗ ModelClient initialization failed: {e}[/red]")
65 |
66 | # Suggest fixes
67 | console.print("\n[bold yellow]=== Suggested Fixes ===[/bold yellow]")
68 | console.print("1. Check if your API provider supports the model name 'dall-e-3'")
69 | console.print("2. Try updating T2I_MODEL_NAME in your configuration:")
70 | console.print(" - For OpenAI-compatible APIs: try 'dall-e' or 'dall-e-2'")
71 | console.print(" - For other providers: check their documentation")
72 | console.print("3. Verify your API endpoint URL is correct")
73 | console.print("4. Test with a simple model name like 'text-to-image'")
74 |
75 | if __name__ == "__main__":
76 | test_t2i_config()
--------------------------------------------------------------------------------
/src/readmex/utils/logo_generator.py:
--------------------------------------------------------------------------------
1 | import os
2 | from rich.console import Console
3 | from readmex.utils.model_client import ModelClient
4 |
5 | def generate_logo(project_dir, descriptions, model_client, console):
6 | """
7 | Generate project logo image based on project description
8 |
9 | Args:
10 | project_dir: Project directory path
11 | descriptions: Project description information
12 | model_client: Model client instance
13 | console: Console output object
14 |
15 | Returns:
16 | str: Generated logo image path, returns None if failed
17 | """
18 | console.print("[cyan]🤖 Generating project logo...[/cyan]")
19 | try:
20 | # Create images directory
21 | images_dir = os.path.join(project_dir, "images")
22 | os.makedirs(images_dir, exist_ok=True)
23 | png_path = os.path.join(images_dir, "logo.png")
24 |
25 | # Step 1: Generate logo description prompt based on project description
26 | description_prompt = f"""Based on the following project information, generate a professional logo design description:
27 |
28 | **Project Information**:
29 | {descriptions}
30 |
31 | Please generate a concise, professional logo design description with the following requirements:
32 | 1. A flat, minimalist vector icon with rectangular outline and rounded corners
33 | 2. Contains geometric shapes that reflect the project's core functionality
34 | 3. Uses smooth gradient colors giving it a modern, tech-inspired look
35 | 4. Clean, simple and modern design, suitable as a project logo
36 | 6. Describe in English, within 50 words
37 |
38 | Return only the logo design description focusing on geometric elements and color scheme, no other explanations.
39 | """
40 |
41 | # Get logo description
42 | try:
43 | logo_description = model_client.get_answer(description_prompt)
44 | except Exception as e:
45 | console.print(f"[red]Failed to get logo description: {e}[/red]")
46 | return None
47 |
48 | # Step 2: Generate image using logo description
49 | image_prompt = f"{logo_description} Don't include any text in the image."
50 | console.print(f"[cyan]Image Prompt: {image_prompt}[/cyan]")
51 | console.print(f"[cyan]Generating image...[/cyan]")
52 |
53 | # Call text-to-image API to generate logo with high quality settings
54 | console.print(f"[cyan]Using high-quality image generation (quality: hd, size: 1024x1024)[/cyan]")
55 | try:
56 | image_result = model_client.get_image(image_prompt)
57 | except Exception as e:
58 | console.print(f"[red]Image generation failed: {e}[/red]")
59 | return None
60 |
61 | if not image_result or not image_result.get("content"):
62 | console.print("[red]Image content is empty, generation failed[/red]")
63 | return None
64 |
65 | # Save image file
66 | with open(png_path, 'wb') as f:
67 | f.write(image_result["content"])
68 |
69 | console.print(f"[green]✔ Logo image saved to {png_path}[/green]")
70 | console.print(f"[green]✔ Image URL: {image_result['url']}[/green]")
71 |
72 | return png_path
73 |
74 | except Exception as e:
75 | console.print(f"[red]Logo generation failed: {e}[/red]")
76 | return None
--------------------------------------------------------------------------------
/tests/test_azure_openai_simple.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Azure OpenAI Integration Tests for ReadmeX
4 | Simplified version to ensure proper execution
5 | """
6 |
7 | import os
8 | import sys
9 | import traceback
10 | from pathlib import Path
11 |
12 | # Add the src directory to Python path
13 | src_path = Path(__file__).parent.parent / "src"
14 | sys.path.insert(0, str(src_path))
15 |
16 | from readmex.utils.model_client import ModelClient
17 | from readmex.config import get_llm_config
18 |
19 | try:
20 | from rich.console import Console
21 | RICH_AVAILABLE = True
22 | except ImportError:
23 | RICH_AVAILABLE = False
24 | class Console:
25 | def print(self, text):
26 | import re
27 | clean_text = re.sub(r'\[.*?\]', '', str(text))
28 | print(clean_text)
29 |
30 |
31 | def test_azure_openai():
32 | """Main test function for Azure OpenAI integration"""
33 | console = Console()
34 |
35 | if RICH_AVAILABLE:
36 | console.print("[bold green]🧪 Azure OpenAI Integration Tests[/bold green]")
37 | else:
38 | console.print("🧪 Azure OpenAI Integration Tests")
39 |
40 | console.print("=" * 50)
41 |
42 | test_results = []
43 |
44 | # Test 1: URL Detection
45 | try:
46 | console.print("\n🔍 Test 1: URL Detection")
47 | client = ModelClient()
48 |
49 | test_urls = [
50 | ("https://api.openai.com/v1", False),
51 | ("https://test.openai.azure.com", True),
52 | ]
53 |
54 | all_passed = True
55 | for url, expected_azure in test_urls:
56 | is_azure = client._is_azure_openai(url)
57 | passed = is_azure == expected_azure
58 | all_passed &= passed
59 |
60 | status = "✅" if passed else "❌"
61 | provider = "Azure" if is_azure else "Standard"
62 | console.print(f" {status} {url} → {provider}")
63 |
64 | test_results.append(("URL Detection", all_passed))
65 |
66 | except Exception as e:
67 | console.print(f"❌ URL Detection failed: {e}")
68 | test_results.append(("URL Detection", False))
69 |
70 | # Test 2: Client Initialization
71 | try:
72 | console.print("\n🔌 Test 2: Client Initialization")
73 | client = ModelClient()
74 |
75 | config = get_llm_config()
76 |
77 | console.print(f" LLM Provider: {'Azure OpenAI' if client.is_llm_azure else 'Standard OpenAI'}")
78 | console.print(f" T2I Provider: {'Azure OpenAI' if client.is_t2i_azure else 'Standard OpenAI'}")
79 | console.print(f" API Key: {config.get('api_key', 'Not set')[:10]}...")
80 |
81 | # Check Azure-specific attributes
82 | success = True
83 | if client.is_llm_azure and hasattr(client, 'llm_deployment'):
84 | console.print(f" ✅ LLM Deployment: {client.llm_deployment}")
85 | elif client.is_llm_azure:
86 | console.print(" ❌ LLM deployment not set")
87 | success = False
88 |
89 | test_results.append(("Client Initialization", success))
90 |
91 | except Exception as e:
92 | console.print(f"❌ Client Initialization failed: {e}")
93 | test_results.append(("Client Initialization", False))
94 |
95 | # Test 3: API Call (if API key available)
96 | try:
97 | console.print("\n🧪 Test 3: API Call Test")
98 | config = get_llm_config()
99 |
100 | if not config.get('api_key'):
101 | console.print(" ⏭️ Skipped - No API key configured")
102 | test_results.append(("API Call", True))
103 | else:
104 | client = ModelClient()
105 | console.print(" 🔄 Testing simple API call...")
106 |
107 | response = client.get_answer("What is 1+1? Answer with just the number.", max_retries=1)
108 | success = response and len(response.strip()) > 0
109 |
110 | if success:
111 | console.print(f" ✅ API call successful: '{response.strip()}'")
112 | else:
113 | console.print(" ❌ API call failed or returned empty response")
114 |
115 | test_results.append(("API Call", success))
116 |
117 | except Exception as e:
118 | console.print(f" ❌ API call failed: {e}")
119 | test_results.append(("API Call", False))
120 |
121 | # Summary
122 | console.print("\n" + "=" * 50)
123 | console.print("📊 Test Results Summary:")
124 |
125 | passed = sum(1 for _, success in test_results if success)
126 | total = len(test_results)
127 |
128 | for test_name, success in test_results:
129 | status = "✅ PASS" if success else "❌ FAIL"
130 | console.print(f" {status} {test_name}")
131 |
132 | console.print(f"\nOverall: {passed}/{total} tests passed")
133 |
134 | if passed == total:
135 | console.print("🎉 All tests passed!")
136 | else:
137 | console.print("⚠️ Some tests failed.")
138 |
139 | return passed == total
140 |
141 |
142 | if __name__ == "__main__":
143 | print("🚀 Starting Azure OpenAI Integration Tests...")
144 | try:
145 | success = test_azure_openai()
146 | print(f"\n✅ Tests completed successfully: {success}")
147 | sys.exit(0 if success else 1)
148 | except Exception as e:
149 | print(f"❌ Test execution failed: {e}")
150 | traceback.print_exc()
151 | sys.exit(1)
152 |
--------------------------------------------------------------------------------
/tests/test_auto_description.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Simple test to verify the auto-generated project description method exists and is callable
4 | """
5 |
6 | import sys
7 | import os
8 |
9 | # Add src to path
10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
11 |
12 | def test_method_exists():
13 | """Test if the _generate_project_description method exists"""
14 | print("=== Testing Auto Project Description Generation ===")
15 |
16 | try:
17 | # Import the core module
18 | from readmex.core import readmex
19 |
20 | # Check if the method exists
21 | if hasattr(readmex, '_generate_project_description'):
22 | print("✅ _generate_project_description method exists")
23 |
24 | # Check method signature
25 | import inspect
26 | sig = inspect.signature(readmex._generate_project_description)
27 | params = list(sig.parameters.keys())
28 |
29 | expected_params = ['self', 'structure', 'dependencies', 'descriptions']
30 | if params == expected_params:
31 | print("✅ Method signature is correct")
32 | print(f" Parameters: {params}")
33 | return True
34 | else:
35 | print(f"❌ Method signature mismatch")
36 | print(f" Expected: {expected_params}")
37 | print(f" Found: {params}")
38 | return False
39 | else:
40 | print("❌ _generate_project_description method not found")
41 | return False
42 |
43 | except ImportError as e:
44 | print(f"❌ Import error: {e}")
45 | return False
46 | except Exception as e:
47 | print(f"❌ Unexpected error: {e}")
48 | return False
49 |
50 | def test_code_changes():
51 | """Test if the code changes were applied correctly"""
52 | print("\n=== Testing Code Changes ===")
53 |
54 | try:
55 | # Read the core.py file to verify changes
56 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
57 |
58 | with open(core_file_path, 'r', encoding='utf-8') as f:
59 | content = f.read()
60 |
61 | # Check for key changes
62 | checks = [
63 | ('_generate_project_description method', '_generate_project_description(self, structure, dependencies, descriptions)'),
64 | ('Auto-generation call in generate method', 'self._generate_project_description(structure, dependencies, descriptions)'),
65 | ('Updated _get_basic_info prompt', 'press Enter to auto-generate'),
66 | ('Updated _get_project_meta_info prompt', 'press Enter to auto-generate')
67 | ]
68 |
69 | all_passed = True
70 | for check_name, check_text in checks:
71 | if check_text in content:
72 | print(f"✅ {check_name}: Found")
73 | else:
74 | print(f"❌ {check_name}: Not found")
75 | all_passed = False
76 |
77 | return all_passed
78 |
79 | except Exception as e:
80 | print(f"❌ Error reading core.py: {e}")
81 | return False
82 |
83 | def test_workflow_logic():
84 | """Test the workflow logic for empty descriptions"""
85 | print("\n=== Testing Workflow Logic ===")
86 |
87 | try:
88 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
89 |
90 | with open(core_file_path, 'r', encoding='utf-8') as f:
91 | content = f.read()
92 |
93 | # Check for the logic in generate method
94 | workflow_checks = [
95 | 'if not self.config["project_description"]:',
96 | 'self.config["project_description"] = self._generate_project_description(',
97 | 'structure, dependencies, descriptions'
98 | ]
99 |
100 | all_found = True
101 | for check in workflow_checks:
102 | if check in content:
103 | print(f"✅ Workflow logic found: {check[:50]}...")
104 | else:
105 | print(f"❌ Workflow logic missing: {check[:50]}...")
106 | all_found = False
107 |
108 | return all_found
109 |
110 | except Exception as e:
111 | print(f"❌ Error checking workflow logic: {e}")
112 | return False
113 |
114 | if __name__ == "__main__":
115 | print("🧪 Testing Auto Project Description Generation Feature")
116 | print("=" * 60)
117 |
118 | # Run tests
119 | test1_result = test_method_exists()
120 | test2_result = test_code_changes()
121 | test3_result = test_workflow_logic()
122 |
123 | # Summary
124 | print("\n" + "=" * 60)
125 | print("📊 Test Summary")
126 | print(f"Test 1 (Method exists): {'✅ PASSED' if test1_result else '❌ FAILED'}")
127 | print(f"Test 2 (Code changes): {'✅ PASSED' if test2_result else '❌ FAILED'}")
128 | print(f"Test 3 (Workflow logic): {'✅ PASSED' if test3_result else '❌ FAILED'}")
129 |
130 | if test1_result and test2_result and test3_result:
131 | print("\n🎉 All tests passed! Auto project description feature has been implemented correctly.")
132 | print("\n📝 Implementation Summary:")
133 | print(" • Added _generate_project_description() method")
134 | print(" • Modified _get_basic_info() to support auto-generation")
135 | print(" • Modified _get_project_meta_info() to support auto-generation")
136 | print(" • Updated generate() method to call auto-generation when description is empty")
137 | print(" • Auto-generation uses project structure, dependencies, and file descriptions")
138 | else:
139 | print("\n⚠️ Some tests failed. Please check the implementation.")
--------------------------------------------------------------------------------
/src/readmex/utils/file_handler.py:
--------------------------------------------------------------------------------
1 | import os
2 | from pathlib import Path
3 | from typing import List, Iterator
4 |
5 | def find_files(
6 | directory: str, patterns: List[str], ignore_patterns: List[str]
7 | ) -> Iterator[str]:
8 | """Find files matching patterns in a directory, excluding ignored ones."""
9 | from fnmatch import fnmatch
10 |
11 | def should_ignore_path(path: str, ignore_patterns: List[str]) -> bool:
12 | """Check if a path should be ignored based on gitignore patterns."""
13 | for pattern in ignore_patterns:
14 | # Handle directory patterns (ending with /)
15 | if pattern.endswith('/'):
16 | dir_pattern = pattern.rstrip('/')
17 | # Check if any part of the path matches the directory pattern
18 | path_parts = path.split(os.sep)
19 | for i in range(len(path_parts)):
20 | # Check if the directory pattern matches at any level
21 | if fnmatch(path_parts[i], dir_pattern):
22 | return True
23 | # Also check if the full path up to this point matches
24 | partial_path = os.sep.join(path_parts[:i+1])
25 | if fnmatch(partial_path, dir_pattern):
26 | return True
27 | else:
28 | # Regular file/path pattern
29 | if fnmatch(path, pattern):
30 | return True
31 | return False
32 |
33 | for root, dirs, files in os.walk(directory):
34 | # Correctly handle directory pruning
35 | dirs[:] = [d for d in dirs if not should_ignore_path(
36 | os.path.relpath(os.path.join(root, d), directory), ignore_patterns
37 | )]
38 |
39 | for basename in files:
40 | # 获取文件的相对路径
41 | rel_path = os.path.relpath(os.path.join(root, basename), directory)
42 | if should_ignore_path(rel_path, ignore_patterns):
43 | continue
44 |
45 | # 检查文件是否匹配所需的模式
46 | if any(fnmatch(basename, pattern) for pattern in patterns):
47 | yield os.path.join(root, basename)
48 |
49 | def _should_ignore_path(path: str, basename: str, ignore_patterns: List[str], is_dir: bool = False) -> bool:
50 | """
51 | 检查路径是否应该被忽略
52 |
53 | Args:
54 | path: 相对路径
55 | basename: 文件或目录名
56 | ignore_patterns: 忽略模式列表
57 | is_dir: 是否为目录
58 |
59 | Returns:
60 | True 如果应该被忽略
61 | """
62 | from fnmatch import fnmatch
63 |
64 | for ignore in ignore_patterns:
65 | # 处理以 / 结尾的模式(专门用于目录)
66 | if ignore.endswith('/'):
67 | dir_pattern = ignore[:-1] # 去掉末尾的 /
68 | if is_dir and (fnmatch(basename, dir_pattern) or fnmatch(path, dir_pattern)):
69 | return True
70 | else:
71 | # 普通模式匹配
72 | if (fnmatch(path, ignore) or
73 | fnmatch(basename, ignore) or
74 | (is_dir and fnmatch(f"{path}/", ignore)) or
75 | (is_dir and fnmatch(f"{basename}/", ignore))):
76 | return True
77 |
78 | return False
79 |
80 |
81 | def get_project_structure(directory: str, ignore_patterns: List[str]) -> str:
82 | """Generate a string representing the project structure."""
83 | from fnmatch import fnmatch
84 |
85 | def should_ignore_path_local(path: str, ignore_patterns: List[str]) -> bool:
86 | """Check if a path should be ignored based on gitignore patterns."""
87 | for pattern in ignore_patterns:
88 | # Handle directory patterns (ending with /)
89 | if pattern.endswith('/'):
90 | dir_pattern = pattern.rstrip('/')
91 | # Check if any part of the path matches the directory pattern
92 | path_parts = path.split(os.sep)
93 | for i in range(len(path_parts)):
94 | # Check if the directory pattern matches at any level
95 | if fnmatch(path_parts[i], dir_pattern):
96 | return True
97 | # Also check if the full path up to this point matches
98 | partial_path = os.sep.join(path_parts[:i+1])
99 | if fnmatch(partial_path, dir_pattern):
100 | return True
101 | else:
102 | # Regular file/path pattern
103 | if fnmatch(path, pattern):
104 | return True
105 | return False
106 | lines = []
107 |
108 | for root, dirs, files in os.walk(directory, topdown=True):
109 | rel_root = os.path.relpath(root, directory)
110 | if rel_root == '.':
111 | rel_root = ''
112 |
113 | # Filter out ignored directories
114 | dirs[:] = [d for d in dirs if not should_ignore_path_local(os.path.join(rel_root, d) if rel_root else d, ignore_patterns)]
115 |
116 | # 过滤文件 - 使用新的忽略逻辑
117 | filtered_files = []
118 | for f in files:
119 | file_path = os.path.join(rel_root, f) if rel_root else f
120 | if not _should_ignore_path(file_path, f, ignore_patterns, is_dir=False):
121 | filtered_files.append(f)
122 |
123 | # 添加当前目录到输出(如果不是根目录)
124 | if rel_root:
125 | level = rel_root.count(os.sep)
126 | indent = " " * level
127 | lines.append(f"{indent}├── {os.path.basename(root)}/")
128 | else:
129 | level = -1
130 | lines.append(f"{os.path.basename(directory)}/")
131 |
132 | # 添加文件到输出
133 | sub_indent = " " * (level + 1)
134 | for f in sorted(filtered_files):
135 | lines.append(f"{sub_indent}├── {f}")
136 |
137 | return "\n".join(lines)
138 |
139 | def load_gitignore_patterns(project_dir: str) -> List[str]:
140 | """Load patterns from .gitignore file."""
141 | gitignore_path = Path(project_dir) / ".gitignore"
142 | if gitignore_path.exists():
143 | with open(gitignore_path, "r") as f:
144 | return [
145 | line.strip()
146 | for line in f
147 | if line.strip() and not line.startswith("#")
148 | ]
149 | return []
--------------------------------------------------------------------------------
/src/readmex/config/ignore_patterns.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": [
3 | ".git",
4 | "__pycache__",
5 | "node_modules",
6 | ".venv",
7 | "venv",
8 | "env",
9 | ".pytest_cache",
10 | ".idea",
11 | ".vscode",
12 | "build",
13 | "dist",
14 | "target",
15 | "bin",
16 | "obj",
17 | ".DS_Store",
18 | ".mypy_cache",
19 | "coverage",
20 | ".coverage",
21 | "htmlcov",
22 | ".tox",
23 | ".eggs",
24 | "*.egg-info",
25 | "*.egg",
26 | ".pytest_cache",
27 | ".cache",
28 | ".next",
29 | ".nuxt",
30 | "out",
31 | ".output",
32 | ".vercel",
33 | ".sass-cache",
34 | ".parcel-cache",
35 | ".eslintcache",
36 | ".stylelintcache",
37 | ".babel-cache",
38 | ".nyc_output",
39 | "coverage",
40 | "lib-cov",
41 | "lcov.info",
42 | "xunit.xml",
43 | "*.tsbuildinfo",
44 | ".turbo",
45 | ".swc",
46 | ".babel",
47 | ".rollup.cache",
48 | ".esbuild",
49 | ".vite",
50 | ".nuxt",
51 | ".next",
52 | "dist-ssr",
53 | "*.local",
54 | ".env.local",
55 | ".env.development.local",
56 | ".env.test.local",
57 | ".env.production.local",
58 | "npm-debug.log*",
59 | "yarn-debug.log*",
60 | "yarn-error.log*",
61 | "lerna-debug.log*",
62 | ".pnpm-debug.log*",
63 | "report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json",
64 | "pids",
65 | "*.pid",
66 | "*.seed",
67 | "*.pid.lock",
68 | "lib-cov",
69 | "coverage",
70 | ".nyc_output",
71 | "grunt-ts",
72 | "node_modules",
73 | "jspm_packages",
74 | "web_modules",
75 | "*.tsbuildinfo",
76 | "*.pyc",
77 | "__pycache__",
78 | "*.so",
79 | "*.egg",
80 | "*.egg-info",
81 | "dist",
82 | "build",
83 | "eggs",
84 | "parts",
85 | "bin",
86 | "var",
87 | "sdist",
88 | "develop-eggs",
89 | ".installed.cfg",
90 | "lib",
91 | "lib64",
92 | "__pycache__",
93 | "*.py[cod]",
94 | "*$py.class",
95 | "*.so",
96 | ".Python",
97 | "build",
98 | "develop-eggs",
99 | "dist",
100 | "downloads",
101 | "eggs",
102 | ".eggs",
103 | "lib",
104 | "lib64",
105 | "parts",
106 | "sdist",
107 | "var",
108 | "wheels",
109 | "share/python-wheels",
110 | "*.egg-info/",
111 | ".installed.cfg",
112 | "*.egg",
113 | "MANIFEST",
114 | "*.manifest",
115 | "*.spec",
116 | "pip-log.txt",
117 | "pip-delete-this-directory.txt",
118 | ".tox/",
119 | ".nox/",
120 | ".coverage",
121 | ".coverage.*",
122 | ".cache",
123 | "nosetests.xml",
124 | "coverage.xml",
125 | "*.cover",
126 | "*.py,cover",
127 | ".hypothesis/",
128 | ".pytest_cache/",
129 | "cover/",
130 | "*.mo",
131 | "*.pot",
132 | "*.log",
133 | "local_settings.py",
134 | "db.sqlite3",
135 | "db.sqlite3-journal",
136 | "instance/",
137 | ".webassets-cache",
138 | ".env",
139 | ".venv",
140 | "env/",
141 | "venv/",
142 | "ENV/",
143 | "env.bak/",
144 | "venv.bak/",
145 | ".spyderproject",
146 | ".spyproject",
147 | "*.swp",
148 | "*.swo",
149 | "*~",
150 | "*.orig",
151 | "*.rej",
152 | "*.bak",
153 | "*.tmp",
154 | "*.temp",
155 | ".DS_Store",
156 | ".DS_Store?",
157 | "_*",
158 | ".Spotlight-V100",
159 | ".Trashes",
160 | "ehthumbs.db",
161 | "Thumbs.db",
162 | "*.lnk",
163 | "*.tmp",
164 | "*.temp",
165 | "*.log",
166 | "*.pid",
167 | "*.seed",
168 | "*.pid.lock"
169 | ],
170 | "ignore_files": [
171 | ".DS_Store",
172 | ".gitignore",
173 | ".gitattributes",
174 | ".editorconfig",
175 | "Thumbs.db",
176 | "desktop.ini",
177 | ".env",
178 | ".env.local",
179 | ".env.production",
180 | ".env.development",
181 | ".env.test",
182 | "*.log",
183 | "*.pid",
184 | "*.seed",
185 | "*.pid.lock",
186 | "*.swp",
187 | "*.swo",
188 | "*~",
189 | "*.orig",
190 | "*.rej",
191 | "*.bak",
192 | "*.tmp",
193 | "*.temp",
194 | "npm-debug.log*",
195 | "yarn-debug.log*",
196 | "yarn-error.log*",
197 | "lerna-debug.log*",
198 | ".pnpm-debug.log*",
199 | "report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json",
200 | "*.tsbuildinfo",
201 | "*.pyc",
202 | "__pycache__",
203 | "*.so",
204 | "*.egg",
205 | "*.egg-info",
206 | "dist",
207 | "build",
208 | "eggs",
209 | "parts",
210 | "bin",
211 | "var",
212 | "sdist",
213 | "develop-eggs",
214 | ".installed.cfg",
215 | "lib",
216 | "lib64",
217 | "__pycache__",
218 | "*.py[cod]",
219 | "*$py.class",
220 | "*.so",
221 | ".Python",
222 | "build",
223 | "develop-eggs",
224 | "dist",
225 | "downloads",
226 | "eggs",
227 | ".eggs",
228 | "lib",
229 | "lib64",
230 | "parts",
231 | "sdist",
232 | "var",
233 | "wheels",
234 | "share/python-wheels",
235 | "*.egg-info/",
236 | ".installed.cfg",
237 | "*.egg",
238 | "MANIFEST",
239 | "*.manifest",
240 | "*.spec",
241 | "pip-log.txt",
242 | "pip-delete-this-directory.txt",
243 | ".tox/",
244 | ".nox/",
245 | ".coverage",
246 | ".coverage.*",
247 | ".cache",
248 | "nosetests.xml",
249 | "coverage.xml",
250 | "*.cover",
251 | "*.py,cover",
252 | ".hypothesis/",
253 | ".pytest_cache/",
254 | "cover/",
255 | "*.mo",
256 | "*.pot",
257 | "*.log",
258 | "local_settings.py",
259 | "db.sqlite3",
260 | "db.sqlite3-journal",
261 | "instance/",
262 | ".webassets-cache",
263 | ".env",
264 | ".venv",
265 | "env/",
266 | "venv/",
267 | "ENV/",
268 | "env.bak/",
269 | "venv.bak/",
270 | ".spyderproject",
271 | ".spyproject",
272 | "*.swp",
273 | "*.swo",
274 | "*~",
275 | "*.orig",
276 | "*.rej",
277 | "*.bak",
278 | "*.tmp",
279 | "*.temp",
280 | ".DS_Store",
281 | ".DS_Store?",
282 | "_*",
283 | ".Spotlight-V100",
284 | ".Trashes",
285 | "ehthumbs.db",
286 | "Thumbs.db",
287 | "*.lnk",
288 | "*.tmp",
289 | "*.temp",
290 | "*.log",
291 | "*.pid",
292 | "*.seed",
293 | "*.pid.lock"
294 | ]
295 | }
--------------------------------------------------------------------------------
/tests/test_core_threading.py:
--------------------------------------------------------------------------------
1 | # tests/test_core_threading.py
2 | # 测试多线程脚本描述生成功能
3 |
4 | import pytest
5 | import os
6 | import tempfile
7 | import time
8 | from unittest.mock import MagicMock, patch
9 | from pathlib import Path
10 | import sys
11 |
12 | # 添加项目根目录到路径
13 | root_dir = Path(__file__).parent.parent
14 | sys.path.append(str(root_dir))
15 |
16 | from src.readmex.core import readmex
17 |
18 |
19 | class TestCoreThreading:
20 | """测试 readmex 的多线程功能"""
21 |
22 | def test_multithreaded_script_descriptions(self):
23 | """测试多线程脚本描述生成"""
24 | print("\n" + "=" * 60)
25 | print("🧵 测试: 多线程脚本描述生成")
26 | print("=" * 60)
27 |
28 | with tempfile.TemporaryDirectory() as temp_dir:
29 | # 创建测试文件
30 | test_files = {
31 | "app.py": "#!/usr/bin/env python\n# Main application file\nprint('Hello World')",
32 | "utils.py": "# Utility functions\ndef helper():\n return True",
33 | "config.py": "# Configuration settings\nDEBUG = True\nAPI_KEY = 'test'",
34 | "models.py": "# Data models\nclass User:\n def __init__(self, name):\n self.name = name"
35 | }
36 |
37 | # 写入测试文件
38 | for filename, content in test_files.items():
39 | filepath = os.path.join(temp_dir, filename)
40 | with open(filepath, 'w', encoding='utf-8') as f:
41 | f.write(content)
42 |
43 | # 创建 readmex 实例并模拟 model_client
44 | craft = readmex(project_dir=temp_dir)
45 |
46 | # Mock model_client.get_answer 方法
47 | def mock_get_answer(prompt):
48 | # 模拟API调用延迟
49 | time.sleep(0.1)
50 | if "app.py" in prompt:
51 | return "主应用程序文件,包含应用启动逻辑"
52 | elif "utils.py" in prompt:
53 | return "工具函数模块,提供辅助功能"
54 | elif "config.py" in prompt:
55 | return "配置管理模块,定义应用配置"
56 | elif "models.py" in prompt:
57 | return "数据模型定义,包含用户类"
58 | else:
59 | return "Python脚本文件"
60 |
61 | craft.model_client.get_answer = mock_get_answer
62 |
63 | print(f"测试目录: {temp_dir}")
64 | print(f"测试文件数量: {len(test_files)}")
65 |
66 | # 测试单线程处理时间
67 | start_time = time.time()
68 | descriptions_single = craft._generate_script_descriptions(max_workers=1)
69 | single_thread_time = time.time() - start_time
70 |
71 | print(f"单线程处理时间: {single_thread_time:.2f} 秒")
72 |
73 | # 测试多线程处理时间
74 | start_time = time.time()
75 | descriptions_multi = craft._generate_script_descriptions(max_workers=3)
76 | multi_thread_time = time.time() - start_time
77 |
78 | print(f"多线程处理时间: {multi_thread_time:.2f} 秒")
79 | print(f"速度提升: {single_thread_time/multi_thread_time:.2f}x")
80 |
81 | # 验证结果
82 | import json
83 | desc_single = json.loads(descriptions_single)
84 | desc_multi = json.loads(descriptions_multi)
85 |
86 | assert len(desc_single) == len(test_files), f"单线程处理文件数不匹配: {len(desc_single)} vs {len(test_files)}"
87 | assert len(desc_multi) == len(test_files), f"多线程处理文件数不匹配: {len(desc_multi)} vs {len(test_files)}"
88 |
89 | # 验证描述内容
90 | for filename in test_files.keys():
91 | assert filename in desc_single, f"单线程结果缺少文件: {filename}"
92 | assert filename in desc_multi, f"多线程结果缺少文件: {filename}"
93 | assert len(desc_single[filename]) > 0, f"单线程描述为空: {filename}"
94 | assert len(desc_multi[filename]) > 0, f"多线程描述为空: {filename}"
95 |
96 | print("✅ 多线程功能测试通过!")
97 | print(f" ✓ 处理文件数: {len(desc_multi)}")
98 | print(f" ✓ 性能提升: {single_thread_time/multi_thread_time:.2f}x")
99 |
100 | # 验证多线程确实比单线程快(考虑到测试环境的误差)
101 | if multi_thread_time < single_thread_time * 0.8: # 至少快20%
102 | print(" ✓ 多线程性能优化有效")
103 | else:
104 | print(" ⚠️ 多线程性能提升不明显(可能由于测试环境限制)")
105 |
106 | def test_empty_file_list(self):
107 | """测试空文件列表的处理"""
108 | print("\n" + "=" * 60)
109 | print("📂 测试: 空文件列表处理")
110 | print("=" * 60)
111 |
112 | with tempfile.TemporaryDirectory() as temp_dir:
113 | # 创建一个只有非脚本文件的目录
114 | with open(os.path.join(temp_dir, "README.md"), 'w') as f:
115 | f.write("# Test Project")
116 |
117 | craft = readmex(project_dir=temp_dir)
118 |
119 | descriptions = craft._generate_script_descriptions(max_workers=3)
120 |
121 | import json
122 | desc_dict = json.loads(descriptions)
123 |
124 | assert len(desc_dict) == 0, "空目录应该返回空字典"
125 | print("✅ 空文件列表处理测试通过!")
126 |
127 | def test_error_handling(self):
128 | """测试错误处理"""
129 | print("\n" + "=" * 60)
130 | print("🚨 测试: 错误处理机制")
131 | print("=" * 60)
132 |
133 | with tempfile.TemporaryDirectory() as temp_dir:
134 | # 创建测试文件
135 | test_file = os.path.join(temp_dir, "test.py")
136 | with open(test_file, 'w', encoding='utf-8') as f:
137 | f.write("print('test')")
138 |
139 | craft = readmex(project_dir=temp_dir)
140 |
141 | # Mock model_client.get_answer 抛出异常
142 | def mock_get_answer_error(prompt):
143 | raise Exception("API调用失败")
144 |
145 | craft.model_client.get_answer = mock_get_answer_error
146 |
147 | # 应该不会崩溃,而是优雅地处理错误
148 | descriptions = craft._generate_script_descriptions(max_workers=2)
149 |
150 | import json
151 | desc_dict = json.loads(descriptions)
152 |
153 | # 由于错误,可能没有成功处理任何文件
154 | print(f"错误情况下处理的文件数: {len(desc_dict)}")
155 | print("✅ 错误处理测试通过!")
156 |
157 |
158 | if __name__ == "__main__":
159 | pytest.main([__file__, "-v", "-s"])
--------------------------------------------------------------------------------
/tests/test_extended_auto_generation.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test script to verify the extended auto-generation functionality for all fields:
4 | - Entry File
5 | - Key Features
6 | - Additional Information
7 | """
8 |
9 | import sys
10 | import os
11 |
12 | def test_new_methods_exist():
13 | """Test if all new auto-generation methods exist in the code"""
14 | print("=== Testing Extended Auto-Generation Methods ===")
15 |
16 | try:
17 | # Read the core.py file to check for method definitions
18 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
19 |
20 | with open(core_file_path, 'r', encoding='utf-8') as f:
21 | content = f.read()
22 |
23 | # Check if all new methods exist
24 | methods_to_check = [
25 | 'def _generate_entry_file(self, structure, dependencies, descriptions):',
26 | 'def _generate_key_features(self, structure, dependencies, descriptions):',
27 | 'def _generate_additional_info(self, structure, dependencies, descriptions):'
28 | ]
29 |
30 | all_found = True
31 | for method_def in methods_to_check:
32 | if method_def in content:
33 | method_name = method_def.split('(')[0].replace('def ', '')
34 | print(f"✅ {method_name} method exists with correct signature")
35 | else:
36 | method_name = method_def.split('(')[0].replace('def ', '')
37 | print(f"❌ {method_name} method does not exist or has incorrect signature")
38 | all_found = False
39 |
40 | return all_found
41 |
42 | except Exception as e:
43 | print(f"❌ Error reading core.py: {e}")
44 | return False
45 |
46 | def test_generate_method_integration():
47 | """Test if the generate method includes all auto-generation calls"""
48 | print("\n=== Testing Generate Method Integration ===")
49 |
50 | try:
51 | # Read the core.py file to check for auto-generation calls
52 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
53 |
54 | with open(core_file_path, 'r', encoding='utf-8') as f:
55 | content = f.read()
56 |
57 | # Check for all auto-generation calls in generate method
58 | auto_gen_calls = [
59 | 'self._generate_entry_file(structure, dependencies, descriptions)',
60 | 'self._generate_key_features(structure, dependencies, descriptions)',
61 | 'self._generate_additional_info(structure, dependencies, descriptions)'
62 | ]
63 |
64 | all_found = True
65 | for call in auto_gen_calls:
66 | if call in content:
67 | print(f"✅ Found auto-generation call: {call}")
68 | else:
69 | print(f"❌ Missing auto-generation call: {call}")
70 | all_found = False
71 |
72 | # Check for conditional logic
73 | conditional_checks = [
74 | 'if not self.config["entry_file"]:',
75 | 'if not self.config["key_features"]:',
76 | 'if not self.config["additional_info"]:'
77 | ]
78 |
79 | for check in conditional_checks:
80 | if check in content:
81 | print(f"✅ Found conditional check: {check}")
82 | else:
83 | print(f"❌ Missing conditional check: {check}")
84 | all_found = False
85 |
86 | return all_found
87 |
88 | except Exception as e:
89 | print(f"❌ Error reading core.py: {e}")
90 | return False
91 |
92 | def test_user_interface_updates():
93 | """Test if user interface prompts have been updated"""
94 | print("\n=== Testing User Interface Updates ===")
95 |
96 | try:
97 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
98 |
99 | with open(core_file_path, 'r', encoding='utf-8') as f:
100 | content = f.read()
101 |
102 | # Check for updated prompts in both _get_basic_info and _get_project_meta_info
103 | expected_prompts = [
104 | 'press Enter to auto-detect', # Entry file
105 | 'press Enter to auto-generate', # Key features and additional info
106 | 'will auto-detect based on project analysis',
107 | 'will auto-generate based on project analysis'
108 | ]
109 |
110 | all_found = True
111 | for prompt in expected_prompts:
112 | if prompt in content:
113 | print(f"✅ Found updated prompt: '{prompt}'")
114 | else:
115 | print(f"❌ Missing updated prompt: '{prompt}'")
116 | all_found = False
117 |
118 | return all_found
119 |
120 | except Exception as e:
121 | print(f"❌ Error checking user interface updates: {e}")
122 | return False
123 |
124 | def test_method_implementations():
125 | """Test if the method implementations contain proper logic"""
126 | print("\n=== Testing Method Implementations ===")
127 |
128 | try:
129 | core_file_path = os.path.join(os.path.dirname(__file__), 'src', 'readmex', 'core.py')
130 |
131 | with open(core_file_path, 'r', encoding='utf-8') as f:
132 | content = f.read()
133 |
134 | # Check for key implementation details
135 | implementation_checks = [
136 | 'Auto-detecting entry file based on project analysis',
137 | 'Auto-generating key features based on project analysis',
138 | 'Auto-generating additional information based on project analysis',
139 | 'self.model_client.get_answer(prompt)',
140 | 'detected_entry = detected_entry.strip()',
141 | 'generated_features = generated_features.strip()',
142 | 'generated_info = generated_info.strip()'
143 | ]
144 |
145 | all_found = True
146 | for check in implementation_checks:
147 | if check in content:
148 | print(f"✅ Found implementation detail: '{check}'")
149 | else:
150 | print(f"❌ Missing implementation detail: '{check}'")
151 | all_found = False
152 |
153 | return all_found
154 |
155 | except Exception as e:
156 | print(f"❌ Error checking method implementations: {e}")
157 | return False
158 |
159 | def main():
160 | """Run all tests"""
161 | print("Testing Extended Auto-Generation Functionality\n")
162 |
163 | tests = [
164 | test_new_methods_exist,
165 | test_generate_method_integration,
166 | test_user_interface_updates,
167 | test_method_implementations
168 | ]
169 |
170 | results = []
171 | for test in tests:
172 | try:
173 | result = test()
174 | results.append(result)
175 | except Exception as e:
176 | print(f"❌ Test failed with exception: {e}")
177 | results.append(False)
178 |
179 | # Summary
180 | print("\n" + "="*50)
181 | print("EXTENDED AUTO-GENERATION TEST SUMMARY")
182 | print("="*50)
183 |
184 | passed = sum(results)
185 | total = len(results)
186 |
187 | if passed == total:
188 | print(f"🎉 All tests passed! ({passed}/{total})")
189 | print("✅ Extended auto-generation functionality is properly implemented")
190 | print("\n📋 Implementation Summary:")
191 | print(" • Entry file auto-detection")
192 | print(" • Key features auto-generation")
193 | print(" • Additional info auto-generation")
194 | print(" • User interface prompts updated")
195 | print(" • Generate method integration complete")
196 | else:
197 | print(f"⚠️ Some tests failed ({passed}/{total})")
198 | print("❌ Extended auto-generation functionality needs attention")
199 |
200 | return passed == total
201 |
202 | if __name__ == "__main__":
203 | success = main()
204 | sys.exit(0 if success else 1)
--------------------------------------------------------------------------------
/tests/test_logo_generator.py:
--------------------------------------------------------------------------------
1 | # tests/test_logo_generator.py
2 | # 测试 logo_generator 的 generate_logo 函数
3 |
4 | import pytest
5 | import os
6 | import tempfile
7 | from unittest.mock import MagicMock, patch
8 | from rich.console import Console
9 | from pathlib import Path
10 | import sys
11 |
12 | root_dir = Path(__file__).parent.parent
13 | sys.path.append(str(root_dir))
14 | from src.readmex.utils.logo_generator import generate_logo
15 | from src.readmex.utils.model_client import ModelClient
16 |
17 |
18 | class TestLogoGenerator:
19 | """测试 Logo 生成器"""
20 |
21 | def test_generate_logo_mock(self):
22 | """使用 Mock 测试 logo 生成逻辑"""
23 | print("\n" + "=" * 60)
24 | print("🧪 测试 1: Mock Logo 生成逻辑测试")
25 | print("=" * 60)
26 |
27 | # 创建临时目录
28 | with tempfile.TemporaryDirectory() as temp_dir:
29 | # 准备测试数据
30 | descriptions = """
31 | {
32 | "main.py": "这是一个自动生成README文档的工具,使用AI技术分析项目结构和代码",
33 | "config.py": "配置文件管理模块,处理API密钥和模型设置"
34 | }
35 | """
36 |
37 | # 创建 Mock 对象
38 | mock_model_client = MagicMock()
39 | console = Console()
40 |
41 | # 模拟 LLM 返回的logo描述
42 | mock_model_client.get_answer.return_value = (
43 | "A modern AI-powered documentation tool logo with blue and green colors"
44 | )
45 |
46 | # 模拟图片生成结果
47 | mock_image_result = {
48 | "url": "https://example.com/logo.png",
49 | "content": b"fake_png_content_123",
50 | }
51 | mock_model_client.get_image.return_value = mock_image_result
52 |
53 | # 调用函数
54 | logo_path = generate_logo(
55 | temp_dir, descriptions, mock_model_client, console
56 | )
57 |
58 | # 验证结果
59 | assert logo_path is not None
60 | assert logo_path.endswith("logo.png")
61 | assert os.path.exists(logo_path)
62 |
63 | # 验证文件内容
64 | with open(logo_path, "rb") as f:
65 | content = f.read()
66 | assert content == b"fake_png_content_123"
67 |
68 | # 验证调用次数
69 | assert mock_model_client.get_answer.call_count == 1
70 | assert mock_model_client.get_image.call_count == 1
71 |
72 | def test_generate_logo_error_handling(self):
73 | """测试错误处理"""
74 | print("\n" + "=" * 60)
75 | print("🚨 测试 2: 错误处理测试")
76 | print("=" * 60)
77 |
78 | with tempfile.TemporaryDirectory() as temp_dir:
79 | descriptions = "测试项目"
80 |
81 | # 创建 Mock 对象,模拟图片生成失败
82 | mock_model_client = MagicMock()
83 | console = Console()
84 |
85 | # 模拟正常的描述生成
86 | mock_model_client.get_answer.return_value = "A test logo"
87 |
88 | # 模拟图片生成失败
89 | mock_image_result = {"error": "API调用失败"}
90 | mock_model_client.get_image.return_value = mock_image_result
91 |
92 | # 调用函数
93 | logo_path = generate_logo(
94 | temp_dir, descriptions, mock_model_client, console
95 | )
96 |
97 | # 验证返回None
98 | assert logo_path is None
99 |
100 | def test_generate_logo_empty_content(self):
101 | """测试图片内容为空的情况"""
102 | print("\n" + "=" * 60)
103 | print("📭 测试 3: 空内容处理测试")
104 | print("=" * 60)
105 |
106 | with tempfile.TemporaryDirectory() as temp_dir:
107 | descriptions = "测试项目"
108 |
109 | mock_model_client = MagicMock()
110 | console = Console()
111 |
112 | # 模拟正常的描述生成
113 | mock_model_client.get_answer.return_value = "A test logo"
114 |
115 | # 模拟图片生成成功但内容为空
116 | mock_image_result = {"url": "https://example.com/logo.png", "content": None}
117 | mock_model_client.get_image.return_value = mock_image_result
118 |
119 | # 调用函数
120 | logo_path = generate_logo(
121 | temp_dir, descriptions, mock_model_client, console
122 | )
123 |
124 | # 验证返回None
125 | assert logo_path is None
126 |
127 | def test_generate_logo_real_api(self):
128 | """使用真实API测试logo生成"""
129 | print("\n" + "=" * 60)
130 | print("🌐 测试 4: 真实 API Logo 生成测试")
131 | print("=" * 60)
132 |
133 | with tempfile.TemporaryDirectory() as temp_dir:
134 | descriptions = """
135 | {
136 | "readmex/core.py": "readmex核心类,负责协调整个README生成流程",
137 | "readmex/utils/model_client.py": "模型客户端,支持LLM问答和AI文生图功能",
138 | "readmex/utils/logo_generator.py": "Logo生成器,根据项目描述生成专业的项目Logo"
139 | }
140 | """
141 |
142 | # 使用真实的 ModelClient
143 | try:
144 | model_client = ModelClient()
145 | console = Console()
146 |
147 | print(f"测试目录: {temp_dir}")
148 | print("开始测试真实API logo生成...")
149 |
150 | logo_path = generate_logo(temp_dir, descriptions, model_client, console)
151 |
152 | # 如果遇到网络问题(SSL错误等),允许测试跳过
153 | if logo_path is None:
154 | pytest.skip("Logo生成失败,可能由于网络连接问题(SSL错误等)")
155 |
156 | # 验证生成结果
157 | assert os.path.exists(logo_path), f"Logo文件不存在: {logo_path}"
158 |
159 | # 检查文件大小
160 | file_size = os.path.getsize(logo_path)
161 | assert file_size > 0, "Logo文件为空"
162 | assert file_size > 1000, f"Logo文件太小,可能生成失败: {file_size} 字节"
163 |
164 | print(f"✅ Logo 生成成功!")
165 | print(f" 文件路径: {logo_path}")
166 | print(f" 文件大小: {file_size:,} 字节")
167 |
168 | # 验证文件是有效的图片格式
169 | with open(logo_path, "rb") as f:
170 | header = f.read(12)
171 |
172 | # 检查常见图片格式
173 | if header.startswith(b"\x89PNG\r\n\x1a\n"):
174 | image_format = "PNG"
175 | elif header.startswith(b"RIFF") and b"WEBP" in header:
176 | image_format = "WebP"
177 | elif header.startswith(b"\xff\xd8\xff"):
178 | image_format = "JPEG"
179 | else:
180 | # 打印文件头以便调试
181 | print(f" 文件头: {header}")
182 | image_format = "Unknown"
183 | # 不直接断言失败,而是警告
184 | print(f" ⚠️ 未知图片格式,但文件大小正常: {file_size:,} 字节")
185 |
186 | if image_format != "Unknown":
187 | print(f" 文件格式: {image_format} ✅")
188 | else:
189 | print(f" 文件格式: 未知但可接受 ⚠️")
190 |
191 | # 验证images目录结构
192 | images_dir = os.path.dirname(logo_path)
193 | assert (
194 | os.path.basename(images_dir) == "images"
195 | ), "Logo应该保存在images目录中"
196 |
197 | print(f" 目录结构: 正确 ✅")
198 | print("🎉 真实API logo生成测试通过!")
199 |
200 | except Exception as e:
201 | # 如果是网络相关错误,跳过测试而不是失败
202 | if (
203 | "SSL" in str(e)
204 | or "ConnectionError" in str(e)
205 | or "TimeoutError" in str(e)
206 | ):
207 | pytest.skip(f"网络连接问题,跳过测试: {e}")
208 | else:
209 | pytest.fail(f"真实API测试失败: {e}")
210 |
211 | def test_logo_description_generation(self):
212 | """测试Logo描述生成功能(仅测试LLM部分)"""
213 | print("\n" + "=" * 60)
214 | print("💬 测试 5: LLM Logo 描述生成测试")
215 | print("=" * 60)
216 |
217 | try:
218 | model_client = ModelClient()
219 | console = Console()
220 |
221 | descriptions = """
222 | {
223 | "app.py": "一个简单的Web应用",
224 | "models.py": "数据库模型定义",
225 | "utils.py": "工具函数集合"
226 | }
227 | """
228 |
229 | print("测试Logo描述生成...")
230 |
231 | # 调用 LLM 生成 logo 描述
232 | prompt = f"""基于以下项目文件描述,为这个项目设计一个专业的logo描述。
233 |
234 | 项目文件描述:
235 | {descriptions}
236 |
237 | 请用英文生成一个详细的logo设计描述,包括:
238 | 1. 视觉元素和符号
239 | 2. 颜色方案
240 | 3. 整体风格
241 | 4. 技术感和专业性
242 |
243 | 描述应该适合用于AI图像生成,清晰明确。"""
244 |
245 | logo_description = model_client.get_answer(prompt)
246 |
247 | # 验证返回结果
248 | assert logo_description is not None, "Logo描述生成失败"
249 | assert len(logo_description.strip()) > 20, "Logo描述太短"
250 | assert isinstance(logo_description, str), "Logo描述应该是字符串"
251 |
252 | print(f"✅ Logo描述生成成功!")
253 | print(f" 描述长度: {len(logo_description)} 字符")
254 | print(f" 描述内容: {logo_description}")
255 |
256 | # 检查描述是否包含一些常见的设计元素词汇
257 | design_keywords = [
258 | "logo",
259 | "color",
260 | "design",
261 | "professional",
262 | "modern",
263 | "symbol",
264 | "icon",
265 | ]
266 | found_keywords = [
267 | word
268 | for word in design_keywords
269 | if word.lower() in logo_description.lower()
270 | ]
271 |
272 | print(f" 包含设计关键词: {', '.join(found_keywords)}")
273 | assert (
274 | len(found_keywords) >= 2
275 | ), f"Logo描述应该包含更多设计相关词汇,当前只有: {found_keywords}"
276 |
277 | print("🎉 Logo描述生成测试通过!")
278 |
279 | except Exception as e:
280 | if "SSL" in str(e) or "ConnectionError" in str(e):
281 | pytest.skip(f"网络连接问题,跳过描述生成测试: {e}")
282 | else:
283 | pytest.fail(f"Logo描述生成测试失败: {e}")
284 |
285 |
286 | if __name__ == "__main__":
287 | pytest.main([__file__, "-v", "-s"])
288 |
--------------------------------------------------------------------------------
/tests/test_complete_dependency_analysis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | 完整的多语言依赖分析测试
4 | 测试项目根目录,语言为 Python
5 | """
6 |
7 | import os
8 | import sys
9 | import tempfile
10 | from pathlib import Path
11 |
12 | # 添加项目根目录到 Python 路径
13 | project_root = Path(__file__).parent.parent # tests 目录的父目录
14 | sys.path.insert(0, str(project_root / "src"))
15 |
16 | from readmex.utils.dependency_analyzer import DependencyAnalyzer
17 | from readmex.utils.model_client import ModelClient
18 |
19 |
20 | def test_complete_dependency_analysis():
21 | """对项目根目录进行完整的依赖分析测试"""
22 | print("=" * 70)
23 | print("完整的多语言依赖分析测试")
24 | print("测试目录: 项目根目录")
25 | print("测试语言: Python")
26 | print("=" * 70)
27 |
28 | # 设置测试目录 - 测试项目根目录
29 | test_project_dir = project_root
30 |
31 | if not test_project_dir.exists():
32 | print(f"❌ 测试目录不存在: {test_project_dir}")
33 | return
34 |
35 | print(f"📁 测试目录: {test_project_dir}")
36 | print()
37 |
38 | try:
39 | # 1. 创建依赖分析器实例
40 | print("1️⃣ 创建 DependencyAnalyzer 实例")
41 |
42 | # 创建真实的 ModelClient 实例
43 | try:
44 | model_client = ModelClient()
45 | print("✅ ModelClient 创建成功")
46 | except Exception as e:
47 | print(f"⚠️ ModelClient 创建失败: {e}")
48 | print(" 将使用 None 作为 model_client")
49 | model_client = None
50 |
51 | analyzer = DependencyAnalyzer(
52 | project_dir=str(test_project_dir),
53 | primary_language="python",
54 | model_client=model_client
55 | )
56 | print(f"✅ 分析器创建成功")
57 | print(f" 当前语言: {analyzer.primary_language}")
58 | print(f" 项目目录: {analyzer.project_dir}")
59 | print(f" ModelClient 状态: {'✅ 可用' if model_client else '❌ 不可用'}")
60 | print()
61 |
62 | # 2. 测试支持的语言
63 | print("2️⃣ 测试支持的语言")
64 | supported_languages = analyzer.get_supported_languages()
65 | print(f"✅ 支持的语言 ({len(supported_languages)} 种):")
66 | for i, lang in enumerate(supported_languages, 1):
67 | print(f" {i:2d}. {lang}")
68 | print()
69 |
70 | # 3. 测试配置加载
71 | print("3️⃣ 测试配置加载")
72 | config = analyzer.config
73 | print(f"✅ 配置加载成功")
74 | print(f" 默认语言: {config['default_language']}")
75 | print(f" Python 配置:")
76 | python_config = config['languages']['python']
77 | print(f" 依赖文件: {python_config['dependency_files']}")
78 | print(f" 文件扩展名: {python_config['file_extensions']}")
79 | print(f" 导入模式数量: {len(python_config['import_patterns'])}")
80 | print()
81 |
82 | # 4. 测试项目导入提取
83 | print("4️⃣ 测试项目导入提取")
84 | imports = analyzer.get_project_imports()
85 | print(f"✅ 发现 {len(imports)} 个导入语句")
86 |
87 | if imports:
88 | print(" 前15个导入语句:")
89 | for i, imp in enumerate(sorted(imports)[:15], 1):
90 | print(f" {i:2d}. {imp}")
91 |
92 | if len(imports) > 15:
93 | print(f" ... 还有 {len(imports) - 15} 个导入语句")
94 | else:
95 | print(" ⚠️ 未发现导入语句")
96 | print()
97 |
98 | # 5. 测试外部依赖过滤
99 | print("5️⃣ 测试外部依赖过滤")
100 | external_imports = analyzer._filter_external_imports(imports)
101 | print(f"✅ 发现 {len(external_imports)} 个外部依赖")
102 |
103 | if external_imports:
104 | print(" 外部依赖列表:")
105 | for i, imp in enumerate(sorted(external_imports), 1):
106 | print(f" {i:2d}. {imp}")
107 | else:
108 | print(" ⚠️ 未发现外部依赖")
109 | print()
110 |
111 | # 6. 测试内置模块过滤
112 | print("6️⃣ 测试内置模块过滤")
113 | builtin_modules = config.get("builtin_modules", {}).get("python", [])
114 | print(f"✅ Python 内置模块数量: {len(builtin_modules)}")
115 | print(" 前10个内置模块:")
116 | for i, module in enumerate(builtin_modules[:10], 1):
117 | print(f" {i:2d}. {module}")
118 | if len(builtin_modules) > 10:
119 | print(f" ... 还有 {len(builtin_modules) - 10} 个内置模块")
120 | print()
121 |
122 | # 7. 测试现有依赖文件检测
123 | print("7️⃣ 测试现有依赖文件检测")
124 | existing_deps = analyzer.get_existing_requirements()
125 | if existing_deps:
126 | print(f"✅ 发现现有依赖文件,内容长度: {len(existing_deps)} 字符")
127 | print(" 依赖文件内容预览:")
128 | preview = existing_deps[:300]
129 | print(f" {preview}")
130 | if len(existing_deps) > 300:
131 | print(" ...")
132 | else:
133 | print(" ℹ️ 未发现现有依赖文件")
134 | print()
135 |
136 | # 8. 测试语言切换功能
137 | print("8️⃣ 测试语言切换功能")
138 | original_lang = analyzer.primary_language
139 |
140 | # 测试切换到 JavaScript
141 | success = analyzer.set_language("javascript")
142 | print(f" 切换到 JavaScript: {'✅ 成功' if success else '❌ 失败'}")
143 | print(f" 当前语言: {analyzer.primary_language}")
144 |
145 | # 测试切换到不支持的语言
146 | success = analyzer.set_language("unsupported_language")
147 | print(f" 切换到不支持的语言: {'❌ 失败' if not success else '✅ 成功'} (预期失败)")
148 | print(f" 当前语言: {analyzer.primary_language}")
149 |
150 | # 切换回原语言
151 | analyzer.set_language(original_lang)
152 | print(f" 切换回 {original_lang}: ✅ 成功")
153 | print()
154 |
155 | # 9. 测试模块名提取
156 | print("9️⃣ 测试模块名提取")
157 | test_imports = [
158 | "import requests",
159 | "from flask import Flask",
160 | "import numpy.array",
161 | "from datetime import datetime"
162 | ]
163 |
164 | print(" 测试模块名提取:")
165 | for imp in test_imports:
166 | module_name = analyzer._extract_module_name(imp)
167 | print(f" '{imp}' -> '{module_name}'")
168 | print()
169 |
170 | # 10. 测试依赖内容清理
171 | print("🔟 测试依赖内容清理")
172 | test_content = """```
173 | Based on analysis:
174 | requests>=2.25.0
175 | flask>=2.0.0
176 | numpy
177 | ```
178 | Some other text
179 | pandas>=1.3.0
180 | """
181 | cleaned = analyzer._clean_dependency_content(test_content)
182 | print(" 原始内容:")
183 | print(f" {repr(test_content)}")
184 | print(" 清理后内容:")
185 | print(f" {repr(cleaned)}")
186 | print()
187 |
188 | # 11. 测试完整的依赖分析流程(包含 LLM 调用)
189 | print("1️⃣1️⃣ 测试完整的依赖分析流程")
190 | try:
191 | # 创建临时输出目录
192 | with tempfile.TemporaryDirectory() as temp_dir:
193 | print(f" 临时输出目录: {temp_dir}")
194 |
195 | if model_client is not None:
196 | print(" 🚀 开始完整的依赖分析(包含 LLM 调用)...")
197 |
198 | # 执行完整的依赖分析
199 | result = analyzer.analyze_project_dependencies(output_dir=temp_dir)
200 |
201 | if result:
202 | print(" ✅ 依赖分析完成!")
203 | print(" ✅ 项目文件扫描完成")
204 | print(" ✅ 导入语句提取完成")
205 | print(" ✅ 外部依赖过滤完成")
206 | print(" ✅ LLM 生成依赖文件完成")
207 | print(" ✅ 依赖文件保存完成")
208 |
209 | # 检查生成的文件
210 | output_path = Path(temp_dir)
211 | generated_files = list(output_path.glob("*"))
212 | print(f" 📁 生成的文件数量: {len(generated_files)}")
213 |
214 | for file in generated_files:
215 | print(f" - {file.name}")
216 | if file.suffix == '.txt' and file.stat().st_size < 1000:
217 | # 显示小文件内容
218 | content = file.read_text()[:200]
219 | print(f" 内容预览: {content}...")
220 | else:
221 | print(" ❌ 依赖分析失败")
222 |
223 | else:
224 | print(" ⚠️ 跳过 LLM 调用部分(model_client=None)")
225 | print(" 测试依赖分析流程的其他部分...")
226 |
227 | # 手动测试各个步骤
228 | print(" ✅ 项目文件扫描完成")
229 | print(" ✅ 导入语句提取完成")
230 | print(" ✅ 外部依赖过滤完成")
231 | print(" ⚠️ LLM 生成跳过(需要 API 密钥)")
232 |
233 | except Exception as e:
234 | print(f" ❌ 分析过程出错: {e}")
235 | import traceback
236 | traceback.print_exc()
237 | print()
238 |
239 | # 12. 生成测试报告
240 | print("1️⃣2️⃣ 测试报告总结")
241 | print("=" * 50)
242 | print(f"📊 测试统计:")
243 | print(f" • 测试目录: {test_project_dir}")
244 | print(f" • 主要语言: {analyzer.primary_language}")
245 | print(f" • 支持语言数: {len(supported_languages)}")
246 | print(f" • 发现导入数: {len(imports)}")
247 | print(f" • 外部依赖数: {len(external_imports)}")
248 | print(f" • 内置模块数: {len(builtin_modules)}")
249 | print(f" • 现有依赖文件: {'是' if existing_deps else '否'}")
250 |
251 | print(f"\n🎯 主要外部依赖:")
252 | if external_imports:
253 | # 提取主要的包名
254 | packages = set()
255 | for imp in external_imports:
256 | module_name = analyzer._extract_module_name(imp)
257 | packages.add(module_name)
258 |
259 | for i, pkg in enumerate(sorted(packages), 1):
260 | print(f" {i:2d}. {pkg}")
261 | else:
262 | print(" 无外部依赖")
263 |
264 | print(f"\n✅ 测试完成!DependencyAnalyzer 功能正常")
265 |
266 | except Exception as e:
267 | print(f"❌ 测试过程中出现错误: {e}")
268 | import traceback
269 | traceback.print_exc()
270 |
271 | print("\n" + "=" * 70)
272 | print("测试结束")
273 | print("=" * 70)
274 |
275 |
276 | if __name__ == "__main__":
277 | test_complete_dependency_analysis()
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [English](README.md) | 简体中文
7 |
8 |
9 |
10 |
11 |
12 |

13 |
14 |
15 | ---
16 |
17 |
18 |
19 |
20 |
23 |
24 |
readmex
25 |
26 |
27 | 🚀 AI智能README生成器:自动为任何仓库创建精美的README和交互式Wiki!可以完全本地化运行,使用你自己的模型。
28 |
29 | 探索文档 »
30 |
31 |
32 |
33 |
34 | [![贡献者][contributors-shield]][contributors-url]
35 | [![分支][forks-shield]][forks-url]
36 | [![星标][stars-shield]][stars-url]
37 | [![问题][issues-shield]][issues-url]
38 |
40 | [![许可证][license-shield]][license-url]
41 |
42 |
43 | 查看演示
44 | ·
45 | 报告Bug
46 | ·
47 | 请求功能
48 |
49 |
50 |
51 |
52 |
53 | 目录
54 |
55 | -
56 | 关于项目
57 |
60 |
61 | -
62 | 快速开始
63 |
67 |
68 | - 使用方法
69 | - 路线图
70 | - 贡献
71 | - 许可证
72 | - 联系方式
73 | - 致谢
74 |
75 |
76 |
77 |
78 | ## 📖 关于项目
79 |
80 | [](https://example.com)
81 |
82 | AI智能README生成器是一个基于AI的工具,可以自动为您的项目生成全面的Markdown README文件。它能够生成结构良好的文档,包括项目详情、技术栈、设置说明、使用示例、徽章、Logo等。
83 |
84 | ### 核心功能
85 |
86 | - 🤖 **AI驱动的README生成**:即时生成全面的Markdown README文档
87 | - 🔗 **自动徽章生成**:创建并嵌入相关的状态徽章(贡献者、分支、星标等)
88 | - 🖼️ **智能Logo设计**:自动生成独特的项目Logo
89 | - 🧠 **技术栈识别**:自动检测并包含项目的技术栈
90 | - 🌐 **上下文感知智能**:根据项目的特定上下文和需求定制内容
91 |
92 | (返回顶部)
93 |
94 | ### 技术栈
95 |
96 | - [![Python][Python]][Python-url]
97 | - [![OpenAI][OpenAI]][OpenAI-url]
98 | - [![Rich][Rich]][Rich-url]
99 |
100 | ### 支持的编程语言
101 |
102 |
103 | 点击展开支持的编程语言
104 |
105 | #### Web开发
106 | - **JavaScript**
107 | - **TypeScript**
108 | - **HTML**
109 | - **CSS**
110 | - **SCSS**
111 | - **Sass**
112 | - **Less**
113 | - **Stylus**
114 | - **Pug**
115 | - **Handlebars**
116 | - **Mustache**
117 | - **Twig**
118 | - **Smarty**
119 | - **Jinja**
120 | - **Vue**
121 |
122 | #### 编程语言
123 | - **Python**
124 | - **Java**
125 | - **C**
126 | - **C++**
127 | - **C#**
128 | - **Go**
129 | - **Rust**
130 | - **PHP**
131 | - **Ruby**
132 | - **Swift**
133 | - **Kotlin**
134 | - **Scala**
135 | - **R**
136 | - **MATLAB**
137 | - **Perl**
138 | - **Lua**
139 | - **Dart**
140 | - **F#**
141 | - **Visual Basic**
142 | - **Assembly**
143 | - **Objective-C**
144 | - **Haskell**
145 | - **Erlang**
146 | - **Elixir**
147 | - **Clojure**
148 | - **CoffeeScript**
149 | - **PowerShell**
150 | - **Shell**
151 | - **Batch**
152 | - **Solidity**
153 |
154 | #### 构建与配置
155 | - **Dockerfile**
156 | - **Makefile**
157 | - **CMake**
158 | - **Gradle**
159 | - **Maven**
160 | - **Nix**
161 | - **Terraform**
162 |
163 | #### 数据与文档
164 | - **Jupyter**
165 | - **Protobuf**
166 | - **GraphQL**
167 | - **WebAssembly**
168 |
169 | #### 编辑器与IDE
170 | - **Vim**
171 | - **Emacs**
172 |
173 |
174 |
175 | (返回顶部)
176 |
177 |
178 | ## 🚀 快速开始
179 |
180 | 以下是在本地设置项目的示例说明。要获取本地副本并运行,请按照以下简单步骤操作。
181 |
182 | ### 前置要求
183 |
184 | - Python 3.7+
185 |
186 | ### 安装
187 |
188 | 1. 使用 pip 安装软件包:
189 | ```bash
190 | pip install readmex
191 | ```
192 |
193 | ### 配置
194 |
195 | `readmex` 需要语言模型(用于生成文本)和文生图模型(用于生成Logo)的API密钥。您可以通过以下两种方式之一进行配置。环境变量的优先级更高。个人信息也可以在全局文件中设置,以作为交互式会话期间的默认值。
196 |
197 | #### 1. 环境变量 (推荐在CI/CD环境中使用)
198 |
199 | 在您的 shell 中设置以下环境变量:
200 |
201 | ```bash
202 | export LLM_API_KEY="your_llm_api_key" # 必填
203 | export T2I_API_KEY="your_t2i_api_key" # 必填
204 |
205 | # 可选:指定自定义API端点和模型
206 | export LLM_BASE_URL="https://api.example.com/v1"
207 | export T2I_BASE_URL="https://api.example.com/v1"
208 | export LLM_MODEL_NAME="your-llm-model"
209 | export T2I_MODEL_NAME="your-t2i-model"
210 |
211 | # 可选:RAG(检索增强生成)的嵌入模型配置
212 | export EMBEDDING_API_KEY="your_embedding_api_key" # 可选,用于Web嵌入模型
213 | export EMBEDDING_BASE_URL="https://api.example.com/v1" # 可选,用于Web嵌入模型
214 | export EMBEDDING_MODEL_NAME="text-embedding-3-small" # 可选,嵌入模型名称
215 | export LOCAL_EMBEDDING="true" # 可选,使用本地嵌入模型(默认:true)
216 |
217 | # 可选:性能配置
218 | export MAX_WORKERS="10" # 可选,最大并发线程数(默认:10)
219 | ```
220 |
221 | #### 2. 全局配置文件 (推荐在本地使用)
222 |
223 | 为了方便,您可以创建一个全局配置文件,工具会自动查找它。
224 |
225 | 1. 创建目录:`mkdir -p ~/.readmex`
226 | 2. 创建配置文件:`~/.readmex/config.json`
227 | 3. 添加您的凭据和任何可选设置:
228 |
229 | ```json
230 | {
231 | "LLM_API_KEY": "在此处填入您的LLM API密钥",
232 | "T2I_API_KEY": "在此处填入您的T2I API密钥",
233 | "LLM_BASE_URL": "https://api.example.com/v1",
234 | "T2I_BASE_URL": "https://api.example.com/v1",
235 | "LLM_MODEL_NAME": "your-llm-model",
236 | "T2I_MODEL_NAME": "your-t2i-model",
237 | "EMBEDDING_API_KEY": "您的嵌入模型API密钥",
238 | "EMBEDDING_BASE_URL": "https://api.example.com/v1",
239 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small",
240 | "LOCAL_EMBEDDING": "true",
241 | "MAX_WORKERS": "10",
242 | "github_username": "您的GitHub用户名",
243 | "twitter_handle": "您的Twitter用户名",
244 | "linkedin_username": "您的LinkedIn用户名",
245 | "email": "您的电子邮箱"
246 | }
247 | ```
248 |
249 | (返回顶部)
250 |
251 |
252 | ## 💻 使用方法
253 |
254 | 有三种方式运行 `readmex` 工具:
255 |
256 | ### 方法 1:安装后直接使用(推荐)
257 |
258 | 安装完成后,您可以在命令行中直接使用 `readmex` 命令:
259 | ```bash
260 | readmex
261 | ```
262 |
263 | ### 方法 2:作为 Python 模块运行
264 |
265 | 如果您没有安装包或想要使用开发版本:
266 | ```bash
267 | # 使用简化的模块调用
268 | python -m readmex
269 |
270 | # 或者使用完整的模块路径
271 | python -m readmex.utils.cli
272 | ```
273 |
274 | ### 方法 3:开发者模式(直接运行脚本)
275 |
276 | 对于开发者或想要直接运行源代码:
277 | ```bash
278 | python src/readmex/utils/cli.py
279 | ```
280 |
281 | ### 命令行选项
282 |
283 | 所有运行方式都支持以下选项:
284 | ```bash
285 | # 基本用法
286 | readmex
287 |
288 | # 指定项目路径和输出目录
289 | readmex --project-path /path/to/your/project --output-dir /path/to/output
290 |
291 | # 生成网站
292 | readmex --website
293 |
294 | # 启动本地服务器
295 | readmex --serve
296 |
297 | # 部署到 GitHub Pages
298 | readmex --deploy
299 |
300 | # 查看帮助
301 | readmex --help
302 | ```
303 |
304 | 这将会:
305 | 1. 生成`project_structure.txt`文件,包含项目结构
306 | 2. 生成`script_description.json`文件,包含项目中脚本的描述
307 | 3. 生成`requirements.txt`文件,包含项目的依赖要求
308 | 4. 生成`logo.png`文件,包含项目的Logo
309 | 5. 生成`README.md`文件,包含项目的README文档
310 |
311 | (返回顶部)
312 |
313 |
314 | ## 🗺️ 路线图
315 |
316 | - [ ] Logo生成的提示工程优化
317 | - [ ] 多语言支持
318 | - [ ] 增强AI对项目功能的描述能力
319 |
320 | 查看[开放问题](https://github.com/aibox22/readmex/issues)以获取提议功能(和已知问题)的完整列表。
321 |
322 | (返回顶部)
323 |
324 |
325 | ## 🤝 贡献
326 |
327 | 贡献让开源社区成为了一个学习、启发和创造的绝佳场所。您所做的任何贡献都是**非常感谢**的。
328 |
329 | 如果您有建议可以改善此项目,请fork该仓库并创建一个pull request。您也可以简单地创建一个带有"enhancement"标签的issue。
330 | 不要忘记给项目点个星!再次感谢!
331 |
332 | 1. Fork此项目
333 | 2. 创建您的功能分支 (`git checkout -b feature/AmazingFeature`)
334 | 3. 提交您的更改 (`git commit -m 'Add some AmazingFeature'`)
335 | 4. 推送到分支 (`git push origin feature/AmazingFeature`)
336 | 5. 开启一个Pull Request
337 |
338 | (返回顶部)
339 |
340 | ### 主要贡献者:
341 |
342 |
343 |
344 |
345 |
346 |
347 | ## 🎗 许可证
348 |
349 | 版权所有 © 2024-2025 [readmex][readmex]。
350 | 基于[MIT][license-url]许可证发布。
351 |
352 | (返回顶部)
353 |
354 |
355 | ## 📧 联系方式
356 |
357 | 邮箱:lintaothu@foxmail.com
358 |
359 | 项目链接:[https://github.com/aibox22/readmex](https://github.com/aibox22/readmex)
360 |
361 | QQ群:2161023585(欢迎加入我们的QQ群进行讨论和获取帮助!)
362 |
363 |
364 |

365 |
扫描二维码加入我们的QQ群
366 |
367 |
368 | (返回顶部)
369 |
370 |
371 | [readmex]: https://github.com/aibox22/readmex
372 |
373 |
374 | [contributors-shield]: https://img.shields.io/github/contributors/aibox22/readmex.svg?style=flat-round
375 | [contributors-url]: https://github.com/aibox22/readmex/graphs/contributors
376 | [forks-shield]: https://img.shields.io/github/forks/aibox22/readmex.svg?style=flat-round
377 | [forks-url]: https://github.com/aibox22/readmex/network/members
378 | [stars-shield]: https://img.shields.io/github/stars/aibox22/readmex.svg?style=flat-round
379 | [stars-url]: https://github.com/aibox22/readmex/stargazers
380 | [issues-shield]: https://img.shields.io/github/issues/aibox22/readmex.svg?style=flat-round
381 | [issues-url]: https://github.com/aibox22/readmex/issues
382 | [release-shield]: https://img.shields.io/github/v/release/aibox22/readmex?style=flat-round
383 | [release-url]: https://github.com/aibox22/readmex/releases
384 | [release-date-shield]: https://img.shields.io/github/release-date/aibox22/readmex?color=9cf&style=flat-round
385 | [license-shield]: https://img.shields.io/github/license/aibox22/readmex.svg?style=flat-round
386 | [license-url]: https://github.com/aibox22/readmex/blob/master/LICENSE.txt
387 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white
388 | [Python-url]: https://www.python.org/
389 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white
390 | [OpenAI-url]: https://openai.com/
391 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white
392 | [Flask-url]: https://flask.palletsprojects.com/
393 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white
394 | [Rich-url]: https://rich.readthedocs.io/
395 |
396 |
397 | ## ⭐ 星标历史
398 |
399 |
404 |
--------------------------------------------------------------------------------
/src/readmex/templates/BLANK_README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ---
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
{{project_title}}
16 |
17 |
18 | {{project_description}}
19 |
20 | Explore the docs »
21 |
22 |
23 |
24 |
25 | [![Contributors][contributors-shield]][contributors-url]
26 | [![Forks][forks-shield]][forks-url]
27 | [![Stargazers][stars-shield]][stars-url]
28 | [![Issues][issues-shield]][issues-url]
29 |
31 | [![License][license-shield]][license-url]
32 |
33 |
34 | View Demo
35 | ·
36 | Report Bug
37 | ·
38 | Request Feature
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Table of Contents
47 |
48 | -
49 | About The Project
50 |
53 |
54 | -
55 | Getting Started
56 |
60 |
61 | - Usage
62 | - Roadmap
63 | - Contributing
64 | - License
65 | - Contact
66 | - Acknowledgments
67 |
68 |
69 |
70 |
71 |
72 |
73 | ## 📖 About The Project
74 |
75 | [](https://example.com)
76 |
77 | {{project_description}}
78 |
79 | ### Key Features
80 |
81 | {{key_features_section}}
82 |
83 | (back to top)
84 |
85 |
86 |
87 | ### Built With
88 |
89 | {{built_with_section}}
90 | for example:
91 | * [![React][React.js]][React-url]
92 | * [![Vue][Vue.js]][Vue-url]
93 |
94 | (back to top)
95 |
96 |
97 |
98 | ### 📁 Project Structure
99 |
100 |
101 | Click to expand project structure
102 |
103 | ```
104 | {{project_structure}}
105 | ```
106 |
107 |
108 |
109 | (back to top)
110 |
111 |
112 |
113 |
114 | ## 🚀 Getting Started
115 |
116 | This is an example of how you may give instructions on setting up your project locally. To get a local copy up and running follow these simple steps.
117 |
118 | ### Prerequisites
119 |
120 | {{prerequisites_section}}
121 |
122 | ### Installation
123 |
124 | {{installation_commands}}
125 |
126 | ### Configuration
127 |
128 | {{configuration_section}}
129 |
130 | (back to top)
131 |
132 |
133 |
134 |
135 | ## 💻 Usage
136 |
137 | {{usage_section}}
138 |
139 | (back to top)
140 |
141 |
142 |
143 |
144 | ## 🗺️ Roadmap
145 |
146 | {{roadmap_section}}
147 |
148 | See the [open issues](https://github.com/{{github_username}}/{{repo_name}}/issues) for a full list of proposed features (and known issues).
149 |
150 | (back to top)
151 |
152 |
153 |
154 |
155 | ## 🤝 Contributing
156 |
157 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
158 |
159 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
160 | Don't forget to give the project a star! Thanks again!
161 |
162 | 1. Fork the Project
163 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
164 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
165 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
166 | 5. Open a Pull Request
167 |
168 | (back to top)
169 |
170 | ### Top contributors:
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | ## 🎗 License
180 |
181 | Copyright © 2024-2025 [{{project_title}}][{{project_title}}].
182 | Released under the [{{project_license}}][license-url] license.
183 |
184 | (back to top)
185 |
186 |
187 |
188 |
189 | ## 📧 Contact
190 |
191 | Email: {{email}}
192 |
193 | Project Link: [https://github.com/{{github_username}}/{{repo_name}}](https://github.com/{{github_username}}/{{repo_name}})
194 |
195 | {{contact_additional_info}}
196 |
197 | (back to top)
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | [{{project_title}}]: https://github.com/{{github_username}}/{{repo_name}}
207 |
208 |
209 | [contributors-shield]: https://img.shields.io/github/contributors/{{github_username}}/{{repo_name}}.svg?style=flat-round
210 | [contributors-url]: https://github.com/{{github_username}}/{{repo_name}}/graphs/contributors
211 | [forks-shield]: https://img.shields.io/github/forks/{{github_username}}/{{repo_name}}.svg?style=flat-round
212 | [forks-url]: https://github.com/{{github_username}}/{{repo_name}}/network/members
213 | [stars-shield]: https://img.shields.io/github/stars/{{github_username}}/{{repo_name}}.svg?style=flat-round
214 | [stars-url]: https://github.com/{{github_username}}/{{repo_name}}/stargazers
215 | [issues-shield]: https://img.shields.io/github/issues/{{github_username}}/{{repo_name}}.svg?style=flat-round
216 | [issues-url]: https://github.com/{{github_username}}/{{repo_name}}/issues
217 | [release-shield]: https://img.shields.io/github/v/release/{{github_username}}/{{repo_name}}?style=flat-round
218 | [release-url]: https://github.com/{{github_username}}/{{repo_name}}/releases
219 | [release-date-shield]: https://img.shields.io/github/release-date/{{github_username}}/{{repo_name}}?color=9cf&style=flat-round
220 | [license-shield]: https://img.shields.io/github/license/{{github_username}}/{{repo_name}}.svg?style=flat-round
221 | [license-url]: https://github.com/{{github_username}}/{{repo_name}}/blob/master/LICENSE.txt
222 |
223 |
224 |
225 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white
226 | [Python-url]: https://www.python.org/
227 | [JavaScript]: https://img.shields.io/badge/JavaScript-F7DF1E?style=flat-round&logo=javascript&logoColor=black
228 | [JavaScript-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript
229 | [TypeScript]: https://img.shields.io/badge/TypeScript-007ACC?style=flat-round&logo=typescript&logoColor=white
230 | [TypeScript-url]: https://www.typescriptlang.org/
231 | [Java]: https://img.shields.io/badge/Java-ED8B00?style=flat-round&logo=openjdk&logoColor=white
232 | [Java-url]: https://www.oracle.com/java/
233 | [Go]: https://img.shields.io/badge/Go-00ADD8?style=flat-round&logo=go&logoColor=white
234 | [Go-url]: https://golang.org/
235 | [Rust]: https://img.shields.io/badge/Rust-000000?style=flat-round&logo=rust&logoColor=white
236 | [Rust-url]: https://www.rust-lang.org/
237 | [C]: https://img.shields.io/badge/C-00599C?style=flat-round&logo=c&logoColor=white
238 | [C-url]: https://en.wikipedia.org/wiki/C_(programming_language)
239 | [CPP]: https://img.shields.io/badge/C++-00599C?style=flat-round&logo=cplusplus&logoColor=white
240 | [CPP-url]: https://en.wikipedia.org/wiki/C%2B%2B
241 | [CSharp]: https://img.shields.io/badge/C%23-239120?style=flat-round&logo=csharp&logoColor=white
242 | [CSharp-url]: https://docs.microsoft.com/en-us/dotnet/csharp/
243 | [MATLAB]: https://img.shields.io/badge/MATLAB-0076A8?style=flat-round&logo=mathworks&logoColor=white
244 | [MATLAB-url]: https://www.mathworks.com/products/matlab.html
245 |
246 |
247 | [React.js]: https://img.shields.io/badge/React-20232A?style=flat-round&logo=react&logoColor=61DAFB
248 | [React-url]: https://reactjs.org/
249 | [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=flat-round&logo=vuedotjs&logoColor=4FC08D
250 | [Vue-url]: https://vuejs.org/
251 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=flat-round&logo=angular&logoColor=white
252 | [Angular-url]: https://angular.io/
253 | [Next.js]: https://img.shields.io/badge/next.js-000000?style=flat-round&logo=nextdotjs&logoColor=white
254 | [Next-url]: https://nextjs.org/
255 |
256 |
257 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white
258 | [Flask-url]: https://flask.palletsprojects.com/
259 | [Django]: https://img.shields.io/badge/Django-092E20?style=flat-round&logo=django&logoColor=white
260 | [Django-url]: https://www.djangoproject.com/
261 | [FastAPI]: https://img.shields.io/badge/FastAPI-005571?style=flat-round&logo=fastapi&logoColor=white
262 | [FastAPI-url]: https://fastapi.tiangolo.com/
263 | [Express.js]: https://img.shields.io/badge/Express.js-404D59?style=flat-round&logo=express&logoColor=white
264 | [Express-url]: https://expressjs.com/
265 | [Spring]: https://img.shields.io/badge/Spring-6DB33F?style=flat-round&logo=spring&logoColor=white
266 | [Spring-url]: https://spring.io/
267 | [Node.js]: https://img.shields.io/badge/Node.js-43853D?style=flat-round&logo=node.js&logoColor=white
268 | [Node-url]: https://nodejs.org/
269 |
270 |
271 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white
272 | [OpenAI-url]: https://openai.com/
273 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white
274 | [Rich-url]: https://rich.readthedocs.io/
275 |
276 |
277 | [PostgreSQL]: https://img.shields.io/badge/PostgreSQL-316192?style=flat-round&logo=postgresql&logoColor=white
278 | [PostgreSQL-url]: https://www.postgresql.org/
279 | [MySQL]: https://img.shields.io/badge/MySQL-00000F?style=flat-round&logo=mysql&logoColor=white
280 | [MySQL-url]: https://www.mysql.com/
281 | [MongoDB]: https://img.shields.io/badge/MongoDB-4EA94B?style=flat-round&logo=mongodb&logoColor=white
282 | [MongoDB-url]: https://www.mongodb.com/
283 | [Redis]: https://img.shields.io/badge/Redis-DC382D?style=flat-round&logo=redis&logoColor=white
284 | [Redis-url]: https://redis.io/
285 | [SQLite]: https://img.shields.io/badge/SQLite-07405E?style=flat-round&logo=sqlite&logoColor=white
286 | [SQLite-url]: https://www.sqlite.org/
287 |
--------------------------------------------------------------------------------
/src/readmex/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from pathlib import Path
4 | from typing import Dict, Union, Optional
5 | from rich.console import Console
6 | from rich.panel import Panel
7 |
8 | # Define the global config path
9 | CONFIG_DIR = Path.home() / ".readmex"
10 | CONFIG_FILE = CONFIG_DIR / "config.json"
11 |
12 | # Initialize console for rich printing
13 | console = Console()
14 |
15 | # Global cache for the loaded config
16 | _config_cache: Optional[Dict[str, str]] = None
17 | _config_sources: Optional[Dict[str, str]] = None
18 |
19 |
20 |
21 | def load_config() -> Dict[str, str]:
22 | """
23 | Load configuration from a global config file and override with environment variables.
24 | Keys from the config file are normalized to lowercase for internal consistency.
25 | """
26 | global _config_cache, _config_sources
27 | if _config_cache is not None:
28 | return _config_cache
29 |
30 | config = {}
31 | sources = {}
32 | # First, try to load from the global config file
33 | if CONFIG_FILE.exists():
34 | with open(CONFIG_FILE, 'r') as f:
35 | try:
36 | file_config = json.load(f)
37 | # Normalize keys to lowercase
38 | config = {k.lower(): v for k, v in file_config.items()}
39 | for key in config:
40 | sources[key] = str(CONFIG_FILE)
41 | except json.JSONDecodeError as e:
42 | console.print(Panel(f"[bold red]Error parsing config file:[/] {CONFIG_FILE}\n[bold]Reason:[/] {e}",
43 | title="[bold yellow]Configuration Error[/bold yellow]", expand=False, border_style="red"))
44 | exit()
45 | except AttributeError:
46 | # Handle cases where the file content is not a dictionary
47 | pass
48 |
49 | # Define mapping from ENV var name (uppercase) to internal config key (lowercase)
50 | env_map = {
51 | "LLM_API_KEY": "llm_api_key",
52 | "LLM_BASE_URL": "llm_base_url",
53 | "LLM_MODEL_NAME": "llm_model_name",
54 | "T2I_API_KEY": "t2i_api_key",
55 | "T2I_BASE_URL": "t2i_base_url",
56 | "T2I_MODEL_NAME": "t2i_model_name",
57 | "EMBEDDING_API_KEY": "embedding_api_key",
58 | "EMBEDDING_BASE_URL": "embedding_base_url",
59 | "EMBEDDING_MODEL_NAME": "embedding_model_name",
60 | "LOCAL_EMBEDDING": "local_embedding",
61 | "MAX_WORKERS": "max_workers",
62 | "GITHUB_USERNAME": "github_username",
63 | "TWITTER_HANDLE": "twitter_handle",
64 | "LINKEDIN_USERNAME": "linkedin_username",
65 | "EMAIL": "email",
66 | }
67 |
68 | # Load from environment, overriding file config if an env var is set
69 | for env_var, config_key in env_map.items():
70 | env_value = os.getenv(env_var)
71 | if env_value is not None:
72 | config[config_key] = env_value
73 | sources[config_key] = f"Environment Variable ({env_var})"
74 |
75 | # Set defaults for personal info if not provided
76 | personal_info_keys = ["github_username", "twitter_handle", "linkedin_username", "email"]
77 | for key in personal_info_keys:
78 | if key not in config:
79 | config[key] = ""
80 |
81 | # Set defaults for embedding config if not provided
82 | embedding_defaults = {
83 | "embedding_model_name": "text-embedding-3-small",
84 | "embedding_base_url": "https://api.openai.com/v1",
85 | "embedding_api_key": "",
86 | "local_embedding": "true"
87 | }
88 | for key, default_value in embedding_defaults.items():
89 | if key not in config:
90 | config[key] = default_value
91 |
92 | # Set defaults for max_workers if not provided
93 | if "max_workers" not in config:
94 | config["max_workers"] = "10"
95 |
96 | _config_cache = config
97 | _config_sources = sources
98 | return _config_cache
99 |
100 | def validate_config():
101 | """Validate that required configurations are present and provide detailed guidance if not."""
102 | config = load_config()
103 | required_keys = ["llm_api_key", "t2i_api_key"]
104 | missing_keys = [key for key in required_keys if not config.get(key)]
105 |
106 | if missing_keys:
107 | console.print(Panel("[bold yellow]Missing required API keys. Let's set them up.[/bold yellow]", title="Configuration Required", expand=False))
108 |
109 | # Interactive input for missing keys
110 | for key in missing_keys:
111 | config[key] = console.input(f"Please enter your [bold cyan]{key}[/bold cyan]: ").strip()
112 |
113 | # After LLM API key is entered, prompt for LLM base URL
114 | if key == "llm_api_key":
115 | llm_base_url = console.input("Please enter your llm_base_url(default is https://api.openai.com/v1): ").strip()
116 | if llm_base_url:
117 | config["llm_base_url"] = llm_base_url
118 |
119 | # Save to config file
120 | CONFIG_DIR.mkdir(exist_ok=True)
121 |
122 | # Load existing config to update it, not overwrite
123 | try:
124 | with open(CONFIG_FILE, 'r') as f_read:
125 | existing_config = json.load(f_read)
126 | except (FileNotFoundError, json.JSONDecodeError):
127 | # Create a complete config template with all possible keys
128 | existing_config = {
129 | "LLM_API_KEY": "",
130 | "LLM_BASE_URL": "https://api.openai.com/v1",
131 | "LLM_MODEL_NAME": "gpt-3.5-turbo",
132 | "T2I_API_KEY": "",
133 | "T2I_BASE_URL": "https://api.openai.com/v1",
134 | "T2I_MODEL_NAME": "dall-e-3",
135 | "EMBEDDING_API_KEY": "",
136 | "EMBEDDING_BASE_URL": "https://api.openai.com/v1",
137 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small",
138 | "LOCAL_EMBEDDING": "true",
139 | "MAX_WORKERS": "10",
140 | "GITHUB_USERNAME": "",
141 | "TWITTER_HANDLE": "",
142 | "LINKEDIN_USERNAME": "",
143 | "EMAIL": ""
144 | }
145 |
146 | # Update with user input
147 | existing_config.update({k.upper(): v for k, v in config.items() if v}) # Save keys in uppercase
148 |
149 | with open(CONFIG_FILE, 'w') as f:
150 | json.dump(existing_config, f, indent=2)
151 |
152 | console.print(f"[green]✔ Configuration saved to [bold cyan]{CONFIG_FILE}[/bold cyan][/green]")
153 |
154 | # Reload config to ensure it's up-to-date
155 | global _config_cache
156 | _config_cache = None
157 | load_config()
158 |
159 |
160 | def get_config_sources() -> Dict[str, str]:
161 | """Returns the sources of the configuration values."""
162 | global _config_sources
163 | if _config_sources is None:
164 | load_config()
165 | return _config_sources
166 |
167 |
168 | def get_llm_config() -> Dict[str, Union[str, int, float]]:
169 | config = load_config()
170 | return {
171 | "model_name": config.get("llm_model_name", "gpt-3.5-turbo"),
172 | "base_url": config.get("llm_base_url", "https://api.openai.com/v1"),
173 | "api_key": config.get("llm_api_key"),
174 | "max_tokens": int(config.get("llm_max_tokens", 1024)),
175 | "temperature": float(config.get("llm_temperature", 0.7)),
176 | }
177 |
178 |
179 | def get_t2i_config() -> Dict[str, Union[str, int, float]]:
180 | config = load_config()
181 | return {
182 | "model_name": config.get("t2i_model_name", "dall-e-3"),
183 | "base_url": config.get("t2i_base_url", "https://api.openai.com/v1"),
184 | "api_key": config.get("t2i_api_key"),
185 | "size": config.get("t2i_image_size", "1024x1024"),
186 | "quality": config.get("t2i_image_quality", "standard"),
187 | }
188 |
189 |
190 | def get_embedding_config() -> Dict[str, Union[str, bool]]:
191 | """获取embedding模型配置"""
192 | config = load_config()
193 | return {
194 | "model_name": config.get("embedding_model_name", "text-embedding-3-small"),
195 | "base_url": config.get("embedding_base_url", "https://api.openai.com/v1"),
196 | "api_key": config.get("embedding_api_key"),
197 | "local_embedding": config.get("local_embedding", "true").lower() == "true",
198 | }
199 |
200 |
201 | def get_max_workers() -> int:
202 | """获取最大并发工作线程数"""
203 | config = load_config()
204 | try:
205 | return int(config.get("max_workers", "10"))
206 | except (ValueError, TypeError):
207 | return 10
208 |
209 |
210 | # Keep original default configurations for use by other modules
211 | DEFAULT_IGNORE_PATTERNS = [
212 | ".git",
213 | ".github",
214 | ".cursor",
215 | ".cursorrules",
216 | ".vscode",
217 | "__pycache__",
218 | "*.pyc",
219 | "*.md",
220 | "website/*",
221 | ".DS_Store",
222 | "build",
223 | "dist",
224 | "*.egg-info",
225 | ".venv",
226 | "venv",
227 | "__init__.py", # 根目录下的 __init__.py
228 | "*/__init__.py", # 一级子目录下的 __init__.py
229 | "*/*/__init__.py", # 二级子目录下的 __init__.py
230 | ".idea",
231 | "*output*"
232 | ]
233 |
234 | # Patterns for script files to be described by the LLM
235 | SCRIPT_PATTERNS = ["*.py", "*.sh", "*.ipynb"]
236 | DOCUMENT_PATTERNS = ["*.md", "*.txt"]
237 |
238 |
239 | def get_readme_template_path():
240 | """Gets the path to the BLANK_README.md template."""
241 | from importlib import resources
242 | import os
243 |
244 | try:
245 | # 尝试使用 importlib.resources (Python 3.9+)
246 | template_files = resources.files('readmex.templates')
247 | if template_files is not None:
248 | template_path = template_files.joinpath('BLANK_README.md')
249 | if template_path.is_file():
250 | return str(template_path)
251 | except (AttributeError, FileNotFoundError, TypeError):
252 | pass
253 |
254 | # 备用方案:使用相对路径
255 | try:
256 | current_dir = os.path.dirname(os.path.abspath(__file__))
257 | template_path = os.path.join(current_dir, 'templates', 'BLANK_README.md')
258 | if os.path.exists(template_path):
259 | return template_path
260 | except Exception:
261 | pass
262 |
263 | # 最后的备用方案:使用 pkg_resources (如果可用)
264 | try:
265 | import pkg_resources
266 | return pkg_resources.resource_filename('readmex.templates', 'BLANK_README.md')
267 | except (ImportError, FileNotFoundError):
268 | pass
269 |
270 | raise FileNotFoundError("BLANK_README.md not found in package templates.")
271 |
272 |
273 | if __name__ == "__main__":
274 | # Test configuration loading
275 | validate_config()
276 | print("=== LLM Configuration ===")
277 | llm_config = get_llm_config()
278 | for key, value in llm_config.items():
279 | print(f"{key}: {value}")
280 |
281 | print("\n=== Text-to-Image Configuration ===")
282 | t2i_config = get_t2i_config()
283 | for key, value in t2i_config.items():
284 | print(f"{key}: {value}")
285 |
286 | print("\n=== Configuration Validation ===")
287 | try:
288 | validate_config()
289 | print("Configuration validation passed")
290 | except SystemExit:
291 | # The validate_config function calls exit(), so we catch it to allow the script to continue
292 | # In a real run, the program would terminate here.
293 | pass
294 | except ValueError as e:
295 | print(f"Configuration validation failed: {e}")
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | English | [简体中文](README_CN.md)
8 |
9 |
10 |
11 |
12 |
13 |

14 |
15 |
16 | ---
17 |
18 |
19 |
20 |
21 |
24 |
25 |
readmex
26 |
27 |
28 | 🚀 AI-Powered README Generator: Automatically creates beautiful READMEs and interactive wikis for any repository! Can run all in local with your own models.
29 |
30 | Explore the docs »
31 |
32 |
33 |
34 |
35 | [![Contributors][contributors-shield]][contributors-url]
36 | [![Forks][forks-shield]][forks-url]
37 | [![Stargazers][stars-shield]][stars-url]
38 | [![Issues][issues-shield]][issues-url]
39 |
41 | [![License][license-shield]][license-url]
42 |
43 |
44 | View Demo
45 | ·
46 | Report Bug
47 | ·
48 | Request Feature
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | Table of Contents
57 |
58 | -
59 | About The Project
60 |
63 |
64 | -
65 | Getting Started
66 |
70 |
71 | - Usage
72 | - Roadmap
73 | - Contributing
74 | - License
75 | - Contact
76 | - Acknowledgments
77 |
78 |
79 |
80 |
81 |
82 |
83 | ## 📖 About The Project
84 |
85 | [](https://example.com)
86 |
87 | AI-Powered README Generator is an AI-powered tool that automatically generates comprehensive Markdown README files for your projects. It crafts well-structured documentation that includes project details, technology stack, setup instructions, usage examples, badges, logos, and more.
88 |
89 | ### Key Features
90 |
91 | - 🤖 **AI-Powered READMEs**: Generate comprehensive Markdown READMEs instantly.
92 | - 🔗 **Auto Badges**: Creates and embeds relevant status badges (contributors, forks, stars, etc.).
93 | - 🖼️ **Smart Logo Design**: Crafts a unique project logo automatically.
94 | - 🧠 **Tech Stack Identification**: Automatically detects and includes the project's technology stack.
95 | - 🌐 **Context-Aware Intelligence**: Tailors content to your project's specific context and needs.
96 |
97 | (back to top)
98 |
99 |
100 |
101 | ### Built With
102 |
103 | - [![Python][Python]][Python-url]
104 | - [![OpenAI][OpenAI]][OpenAI-url]
105 | - [![Rich][Rich]][Rich-url]
106 |
107 | ### Supported Programming Languages
108 |
109 |
110 | Click to expand supported languages
111 |
112 | #### Web Development
113 | - **JavaScript**
114 | - **TypeScript**
115 | - **HTML**
116 | - **CSS**
117 | - **SCSS**
118 | - **Sass**
119 | - **Less**
120 | - **Stylus**
121 | - **Pug**
122 | - **Handlebars**
123 | - **Mustache**
124 | - **Twig**
125 | - **Smarty**
126 | - **Jinja**
127 | - **Vue**
128 |
129 | #### Programming Languages
130 | - **Python**
131 | - **Java**
132 | - **C**
133 | - **C++**
134 | - **C#**
135 | - **Go**
136 | - **Rust**
137 | - **PHP**
138 | - **Ruby**
139 | - **Swift**
140 | - **Kotlin**
141 | - **Scala**
142 | - **R**
143 | - **MATLAB**
144 | - **Perl**
145 | - **Lua**
146 | - **Dart**
147 | - **F#**
148 | - **Visual Basic**
149 | - **Assembly**
150 | - **Objective-C**
151 | - **Haskell**
152 | - **Erlang**
153 | - **Elixir**
154 | - **Clojure**
155 | - **CoffeeScript**
156 | - **PowerShell**
157 | - **Shell**
158 | - **Batch**
159 | - **Solidity**
160 |
161 | #### Build & Configuration
162 | - **Dockerfile**
163 | - **Makefile**
164 | - **CMake**
165 | - **Gradle**
166 | - **Maven**
167 | - **Nix**
168 | - **Terraform**
169 |
170 | #### Data & Documentation
171 | - **Jupyter**
172 | - **Protobuf**
173 | - **GraphQL**
174 | - **WebAssembly**
175 |
176 | #### Editor & IDE
177 | - **Vim**
178 | - **Emacs**
179 |
180 |
181 |
182 | (back to top)
183 |
184 |
185 |
186 |
187 | ## 🚀 Getting Started
188 |
189 | This is an example of how you may give instructions on setting up your project locally. To get a local copy up and running follow these simple steps.
190 |
191 | ### Prerequisites
192 |
193 | - Python 3.7+
194 |
195 | ### Installation
196 |
197 | 1. Install the package using pip:
198 | ```bash
199 | pip install readmex
200 | ```
201 | ### Configuration
202 |
203 | `readmex` requires API keys for both the Language Model (for generating text) and the Text-to-Image model (for generating logos). You can configure these in one of two ways. Environment variables take precedence.
204 |
205 | #### 1. Environment Variables (Recommended for CI/CD)
206 |
207 | Set the following environment variables in your shell:
208 |
209 | ```bash
210 | export LLM_API_KEY="your_llm_api_key" # Required
211 | export T2I_API_KEY="your_t2i_api_key" # Required
212 |
213 | # Optional: Specify custom API endpoints and models
214 | export LLM_BASE_URL="https://api.example.com/v1"
215 | export T2I_BASE_URL="https://api.example.com/v1"
216 | export LLM_MODEL_NAME="your-llm-model"
217 | export T2I_MODEL_NAME="your-t2i-model"
218 |
219 | # Optional: Embedding model configuration for RAG (Retrieval-Augmented Generation)
220 | export EMBEDDING_API_KEY="your_embedding_api_key" # Optional, for web embedding models
221 | export EMBEDDING_BASE_URL="https://api.example.com/v1" # Optional, for web embedding models
222 | export EMBEDDING_MODEL_NAME="text-embedding-3-small" # Optional, embedding model name
223 | export LOCAL_EMBEDDING="true" # Optional, use local embedding model (default: true)
224 |
225 | # Optional: Performance configuration
226 | export MAX_WORKERS="10" # Optional, max concurrent threads (default: 10)
227 | ```
228 |
229 | #### 2. Global Config File (Recommended for Local Use)
230 |
231 | For convenience, you can create a global configuration file. The tool will automatically look for it.
232 |
233 | 1. Create the directory: `mkdir -p ~/.readmex`
234 | 2. Create the config file: `~/.readmex/config.json`
235 | 3. Add your credentials and any optional settings. You can also include personal information, which will be used as defaults during interactive prompts:
236 |
237 | ```json
238 | {
239 | "LLM_API_KEY": "your_llm_api_key",
240 | "T2I_API_KEY": "your_t2i_api_key",
241 | "LLM_BASE_URL": "https://api.example.com/v1",
242 | "T2I_BASE_URL": "https://api.example.com/v1",
243 | "LLM_MODEL_NAME": "gpt-4",
244 | "T2I_MODEL_NAME": "dall-e-3",
245 | "EMBEDDING_API_KEY": "your_embedding_api_key",
246 | "EMBEDDING_BASE_URL": "https://api.example.com/v1",
247 | "EMBEDDING_MODEL_NAME": "text-embedding-3-small",
248 | "LOCAL_EMBEDDING": "true",
249 | "MAX_WORKERS": "10",
250 | "github_username": "your_github_username",
251 | "twitter_handle": "your_twitter_handle",
252 | "linkedin_username": "your_linkedin_username",
253 | "email": "your_email@example.com"
254 | }
255 | ```
256 |
257 | (back to top)
258 |
259 |
260 |
261 |
262 | ## 💻 Usage
263 |
264 | Once installed, you can use the `readmex` package in the command line. To generate your README, run the following:
265 |
266 | ### Method 1: Using the installed command (Recommended)
267 | ```bash
268 | readmex
269 | ```
270 |
271 | ### Method 2: Running as a Python module
272 | ```bash
273 | # Run the package directly
274 | python -m readmex
275 |
276 | # Or run the CLI module specifically
277 | python -m readmex.utils.cli
278 | ```
279 |
280 | ### Method 3: Development mode (for contributors)
281 | ```bash
282 | # From the project root directory
283 | python src/readmex/utils/cli.py
284 | ```
285 |
286 | ### Command Line Options
287 |
288 | All methods support the same command line arguments:
289 |
290 | ```bash
291 | # Interactive mode (default)
292 | readmex
293 |
294 | # Generate for current directory
295 | readmex .
296 |
297 | # Generate for specific directory
298 | readmex /path/to/your/project
299 |
300 | # Generate MkDocs website
301 | readmex --website
302 |
303 | # Generate website and serve locally
304 | readmex --website --serve
305 |
306 | # Deploy to GitHub Pages
307 | readmex --deploy
308 |
309 | # Enable debug mode (skip LLM calls for testing)
310 | readmex --debug
311 |
312 | # Enable silent mode (auto-generate without prompts)
313 | readmex --silent
314 |
315 | # Enable verbose mode (show detailed information)
316 | readmex --verbose
317 | ```
318 |
319 | This will:
320 | 1. generate a `project_structure.txt` file, which contains the project structure.
321 | 2. generate a `script_description.json` file, which contains the description of the scripts in the project.
322 | 3. generate a `requirements.txt` file, which contains the requirements of the project.
323 | 4. generate a `logo.png` file, which contains the logo of the project.
324 | 5. generate a `README.md` file, which contains the README of the project.
325 |
326 | (back to top)
327 |
328 |
329 |
330 |
331 | ## 🗺️ Roadmap
332 |
333 | - [ ] Prompt Engineering for Logo Generation
334 | - [ ] Multi-language Support
335 | - [ ] Enhanced AI Descriptions for Project Features
336 |
337 | See the [open issues](https://github.com/aibox22/readmex/issues) for a full list of proposed features (and known issues).
338 |
339 | (back to top)
340 |
341 |
342 |
343 |
344 | ## 🤝 Contributing
345 |
346 | Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
347 |
348 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
349 | Don't forget to give the project a star! Thanks again!
350 |
351 | 1. Fork the Project
352 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
353 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
354 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
355 | 5. Open a Pull Request
356 |
357 | (back to top)
358 |
359 | ### Top contributors:
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 | ## 🎗 License
369 |
370 | Copyright © 2024-2025 [readmex][readmex].
371 | Released under the [MIT][license-url] license.
372 |
373 | (back to top)
374 |
375 |
376 |
377 |
378 | ## 📧 Contact
379 |
380 | Email: lintaothu@foxmail.com
381 |
382 | Project Link: [https://github.com/aibox22/readmex](https://github.com/aibox22/readmex)
383 |
384 | QQ Group: 2161023585 (Welcome to join our QQ Group to discuss and get help!)
385 |
386 |
387 |

388 |
Scan QR code to join our QQ Group
389 |
390 |
391 | (back to top)
392 |
393 |
394 |
395 | [readmex]: https://github.com/aibox22/readmex
396 |
397 |
398 | [contributors-shield]: https://img.shields.io/github/contributors/aibox22/readmex.svg?style=flat-round
399 | [contributors-url]: https://github.com/aibox22/readmex/graphs/contributors
400 | [forks-shield]: https://img.shields.io/github/forks/aibox22/readmex.svg?style=flat-round
401 | [forks-url]: https://github.com/aibox22/readmex/network/members
402 | [stars-shield]: https://img.shields.io/github/stars/aibox22/readmex.svg?style=flat-round
403 | [stars-url]: https://github.com/aibox22/readmex/stargazers
404 | [issues-shield]: https://img.shields.io/github/issues/aibox22/readmex.svg?style=flat-round
405 | [issues-url]: https://github.com/aibox22/readmex/issues
406 | [release-shield]: https://img.shields.io/github/v/release/aibox22/readmex?style=flat-round
407 | [release-url]: https://github.com/aibox22/readmex/releases
408 | [release-date-shield]: https://img.shields.io/github/release-date/aibox22/readmex?color=9cf&style=flat-round
409 | [license-shield]: https://img.shields.io/github/license/aibox22/readmex.svg?style=flat-round
410 | [license-url]: https://github.com/aibox22/readmex/blob/master/LICENSE.txt
411 | [Python]: https://img.shields.io/badge/Python-3776AB?style=flat-round&logo=python&logoColor=white
412 | [Python-url]: https://www.python.org/
413 | [OpenAI]: https://img.shields.io/badge/OpenAI-000000?style=flat-round&logo=openai&logoColor=white
414 | [OpenAI-url]: https://openai.com/
415 | [Flask]: https://img.shields.io/badge/Flask-000000?style=flat-round&logo=flask&logoColor=white
416 | [Flask-url]: https://flask.palletsprojects.com/
417 | [Rich]: https://img.shields.io/badge/Rich-000000?style=flat-round&logo=rich&logoColor=white
418 | [Rich-url]: https://rich.readthedocs.io/
419 |
420 |
421 | ## ⭐ Star History
422 |
423 |
428 |
--------------------------------------------------------------------------------
/src/readmex/utils/cli.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import os
3 | import sys
4 | from pathlib import Path
5 | from rich.console import Console
6 | from rich.table import Table
7 |
8 | # Add the src directory to the Python path when running directly
9 | if __name__ == '__main__':
10 | # Get the project root directory (3 levels up from this file)
11 | project_root = Path(__file__).parent.parent.parent.parent
12 | src_path = project_root / "src"
13 | if src_path not in sys.path:
14 | sys.path.insert(0, str(src_path))
15 |
16 | from readmex.core import readmex
17 | from readmex.website_core import WebsiteGenerator
18 | from readmex.config import validate_config, get_config_sources
19 |
20 | def main():
21 | """
22 | readmex command line entry point
23 | Support both command line arguments and interactive interface
24 | """
25 | parser = argparse.ArgumentParser(
26 | description="readmex - AI-driven README documentation generator",
27 | epilog="Examples:\n readmex # Interactive mode\n readmex . # Generate for current directory\n readmex ./my-project # Generate for specific directory\n readmex --website # Generate MkDocs website\n readmex --website --serve # Generate and serve website",
28 | formatter_class=argparse.RawDescriptionHelpFormatter
29 | )
30 | parser.add_argument(
31 | "project_path",
32 | nargs="?",
33 | help="Path of project for generating README (default: interactive input)"
34 | )
35 | parser.add_argument(
36 | "--website",
37 | action="store_true",
38 | help="Generate MkDocs website instead of README"
39 | )
40 | parser.add_argument(
41 | "--serve",
42 | action="store_true",
43 | help="Start local server after generating website (requires --website)"
44 | )
45 | parser.add_argument(
46 | "--deploy",
47 | action="store_true",
48 | help="Deploy website to GitHub Pages (requires --website)"
49 | )
50 | parser.add_argument(
51 | "--version",
52 | action="version",
53 | version="readmex 0.1.8"
54 | )
55 | parser.add_argument(
56 | "--verbose",
57 | action="store_true",
58 | help="Enable verbose mode to print prompts and detailed information"
59 | )
60 | parser.add_argument(
61 | "--debug",
62 | action="store_true",
63 | help="Enable debug mode to skip LLM calls for faster testing"
64 | )
65 | parser.add_argument(
66 | "--silent",
67 | action="store_true",
68 | help="Enable silent mode to skip interactive prompts (auto-generate all content)"
69 | )
70 |
71 | args = parser.parse_args()
72 |
73 | try:
74 | validate_config()
75 | console = Console()
76 |
77 | # Determine project path
78 | if args.project_path:
79 | project_path = os.path.abspath(args.project_path)
80 | if not os.path.isdir(project_path):
81 | console.print(f"[bold red]Error: Project path '{project_path}' is not a valid directory.[/bold red]")
82 | return
83 | elif args.serve:
84 | # 对于 --serve 参数,直接使用当前目录
85 | project_path = os.getcwd()
86 | else:
87 | console.print("[bold cyan]readmex - AI README Generator[/bold cyan]")
88 | console.print("Please provide the path of project for generating README (press Enter to use the current directory).\n")
89 | project_input = console.input("[cyan]Project Path[/cyan]: ").strip()
90 | project_path = os.path.abspath(project_input) if project_input else os.getcwd()
91 |
92 | if not os.path.isdir(project_path):
93 | console.print(f"[bold red]Error: Project path '{project_path}' is not a valid directory.[/bold red]")
94 | return
95 |
96 | if args.website:
97 | # 网站生成模式
98 | _handle_website_generation(args, project_path, console)
99 | elif args.serve:
100 | # 仅启动服务模式
101 | _handle_serve_only(project_path, console)
102 | else:
103 | # README生成模式
104 | generator = readmex(
105 | project_dir=project_path,
106 | debug=getattr(args, 'debug', False),
107 | silent=getattr(args, 'silent', False)
108 | )
109 | generator.generate()
110 | except KeyboardInterrupt:
111 | console = Console()
112 | console.print("\n[yellow]Operation cancelled[/yellow]")
113 | except FileNotFoundError as e:
114 | console = Console()
115 | console.print(f"[red]Error: {e}[/red]")
116 | except Exception as e:
117 | console = Console()
118 | console.print(f"[bold red]An error occurred: {e}[/bold red]")
119 |
120 | # Show configuration information to help with debugging
121 | from readmex.config import load_config
122 | try:
123 | config = load_config()
124 | sources = get_config_sources()
125 | if config and sources:
126 | # Show configuration source info once
127 | console.print("\n[yellow]Configuration loaded from:[/yellow]")
128 | source_files = set(sources.values())
129 | for source_file in source_files:
130 | if "Environment Variable" not in source_file:
131 | console.print(f"[yellow] • {source_file}[/yellow]")
132 |
133 | # Show configuration table with actual values
134 | table = Table(title="[bold cyan]Current Configuration[/bold cyan]")
135 | table.add_column("Variable", style="cyan")
136 | table.add_column("Value", style="green")
137 |
138 | # Only show non-sensitive configuration values
139 | display_keys = ["llm_model_name", "t2i_model_name", "llm_base_url", "t2i_base_url",
140 | "embedding_model_name", "embedding_base_url", "local_embedding",
141 | "github_username", "twitter_handle", "linkedin_username", "email"]
142 |
143 | for key in display_keys:
144 | if key in config and config[key]:
145 | value = config[key]
146 | # Mask API keys for security
147 | if "api_key" in key.lower():
148 | value = "***" + value[-4:] if len(value) > 4 else "***"
149 | table.add_row(key, value)
150 |
151 | console.print(table)
152 | except Exception:
153 | pass # Don't show config info if there's an error loading it
154 |
155 |
156 | def _handle_serve_only(project_path: str, console: Console) -> None:
157 | """处理仅启动服务功能"""
158 | try:
159 | # 创建网站生成器以获取输出目录
160 | website_generator = WebsiteGenerator(project_path)
161 |
162 | # 检查网站是否存在
163 | website_exists = os.path.exists(website_generator.output_dir) and \
164 | os.path.exists(os.path.join(website_generator.output_dir, "mkdocs.yml")) and \
165 | os.path.exists(os.path.join(website_generator.output_dir, "docs"))
166 |
167 | if not website_exists:
168 | console.print("[red]未找到已生成的网站。请先运行 'readmex --website' 生成网站。[/red]")
169 | return
170 |
171 | console.print("[green]启动网站服务...[/green]")
172 | _serve_website(website_generator.output_dir, console)
173 |
174 | except Exception as e:
175 | console.print(f"[red]启动服务失败: {e}[/red]")
176 | raise
177 |
178 |
179 | def _handle_website_generation(args, project_path: str, console: Console) -> None:
180 | """处理网站生成相关功能"""
181 | try:
182 | # 验证参数组合
183 | if args.deploy and not args.website:
184 | console.print("[red]Error: --deploy requires --website[/red]")
185 | return
186 |
187 | # 创建网站生成器
188 | website_generator = WebsiteGenerator(project_path, verbose=getattr(args, 'verbose', False), debug=getattr(args, 'debug', False))
189 |
190 | # 检查是否只需要启动服务且网站已存在
191 | website_exists = os.path.exists(website_generator.output_dir) and \
192 | os.path.exists(os.path.join(website_generator.output_dir, "mkdocs.yml")) and \
193 | os.path.exists(os.path.join(website_generator.output_dir, "docs"))
194 |
195 | if args.serve and website_exists and not args.deploy:
196 | # 如果只是要启动服务且网站已存在,直接启动服务
197 | console.print("[green]检测到已存在的网站,直接启动服务...[/green]")
198 | _serve_website(website_generator.output_dir, console)
199 | return
200 |
201 | # 生成网站
202 | website_generator.generate_website()
203 |
204 | # 处理后续操作
205 | if args.serve:
206 | _serve_website(website_generator.output_dir, console)
207 | elif args.deploy:
208 | _deploy_website(website_generator.output_dir, console)
209 |
210 | except Exception as e:
211 | console.print(f"[red]网站生成失败: {e}[/red]")
212 | raise
213 |
214 |
215 | def _serve_website(website_dir: str, console: Console) -> None:
216 | """启动本地服务器"""
217 | try:
218 | import subprocess
219 | import webbrowser
220 | import time
221 | import socket
222 |
223 | console.print("[cyan]启动本地服务器...[/cyan]")
224 |
225 | # 检查是否安装了mkdocs
226 | try:
227 | subprocess.run(["mkdocs", "--version"],
228 | capture_output=True, check=True)
229 | except (subprocess.CalledProcessError, FileNotFoundError):
230 | console.print("[yellow]未找到mkdocs,正在安装...[/yellow]")
231 | subprocess.run(["pip3.9", "install", "mkdocs", "mkdocs-material", "mkdocs-drawio"],
232 | check=True)
233 |
234 | # 检查端口是否被占用
235 | def is_port_in_use(port):
236 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
237 | return s.connect_ex(('localhost', port)) == 0
238 |
239 | port = 8000
240 | if is_port_in_use(port):
241 | console.print(f"[yellow]端口 {port} 已被占用,尝试使用其他端口...[/yellow]")
242 | for p in range(8001, 8010):
243 | if not is_port_in_use(p):
244 | port = p
245 | break
246 | else:
247 | console.print("[red]无法找到可用端口[/red]")
248 | return
249 |
250 | # 启动服务器
251 | console.print("[green]服务器启动中,请稍候...[/green]")
252 | process = subprocess.Popen(
253 | ["mkdocs", "serve", "-a", f"127.0.0.1:{port}"],
254 | cwd=website_dir,
255 | stdout=subprocess.PIPE,
256 | stderr=subprocess.STDOUT,
257 | text=True
258 | )
259 |
260 | # 等待服务器启动并检查是否成功
261 | max_wait_time = 10
262 | start_time = time.time()
263 | server_started = False
264 |
265 | while time.time() - start_time < max_wait_time:
266 | if process.poll() is not None:
267 | # 进程已退出,说明启动失败
268 | output = process.stdout.read() if process.stdout else ""
269 | console.print(f"[red]服务器启动失败: {output}[/red]")
270 | return
271 |
272 | if is_port_in_use(port):
273 | server_started = True
274 | break
275 |
276 | time.sleep(0.5)
277 |
278 | if not server_started:
279 | console.print("[red]服务器启动超时[/red]")
280 | process.terminate()
281 | return
282 |
283 | # 打开浏览器
284 | url = f"http://127.0.0.1:{port}"
285 | console.print(f"[green]✅ 服务器已启动: {url}[/green]")
286 |
287 | open_browser = console.input(
288 | "[cyan]是否在浏览器中打开网站? (Y/n): [/cyan]"
289 | ).strip().lower()
290 |
291 | if open_browser in ['', 'y', 'yes', '是']:
292 | webbrowser.open(url)
293 |
294 | console.print("[yellow]按 Ctrl+C 停止服务器[/yellow]")
295 |
296 | try:
297 | # 持续监控服务器状态
298 | while process.poll() is None:
299 | time.sleep(1)
300 | except KeyboardInterrupt:
301 | console.print("\n[yellow]正在停止服务器...[/yellow]")
302 | process.terminate()
303 | try:
304 | process.wait(timeout=5)
305 | except subprocess.TimeoutExpired:
306 | process.kill()
307 | console.print("[green]服务器已停止[/green]")
308 |
309 | except Exception as e:
310 | console.print(f"[red]启动服务器失败: {e}[/red]")
311 |
312 |
313 | def _deploy_website(website_dir: str, console: Console) -> None:
314 | """部署网站到GitHub Pages"""
315 | try:
316 | import subprocess
317 |
318 | console.print("[cyan]准备部署到GitHub Pages...[/cyan]")
319 |
320 | # 检查是否在git仓库中
321 | try:
322 | subprocess.run(["git", "status"],
323 | cwd=website_dir,
324 | capture_output=True,
325 | check=True)
326 | except subprocess.CalledProcessError:
327 | console.print("[red]Error: 当前目录不是git仓库[/red]")
328 | return
329 |
330 | # 检查是否安装了mkdocs
331 | try:
332 | subprocess.run(["mkdocs", "--version"],
333 | capture_output=True, check=True)
334 | except (subprocess.CalledProcessError, FileNotFoundError):
335 | console.print("[yellow]未找到mkdocs,正在安装...[/yellow]")
336 | subprocess.run(["pip", "install", "mkdocs", "mkdocs-material", "mkdocs-drawio"],
337 | check=True)
338 |
339 | # 部署到gh-pages分支
340 | console.print("[cyan]正在部署...[/cyan]")
341 | result = subprocess.run(
342 | ["mkdocs", "gh-deploy", "--clean"],
343 | cwd=website_dir,
344 | capture_output=True,
345 | text=True
346 | )
347 |
348 | if result.returncode == 0:
349 | console.print("[green]✅ 网站已成功部署到GitHub Pages[/green]")
350 | console.print("[cyan]通常需要几分钟时间生效[/cyan]")
351 | else:
352 | console.print(f"[red]部署失败: {result.stderr}[/red]")
353 |
354 | except Exception as e:
355 | console.print(f"[red]部署失败: {e}[/red]")
356 |
357 |
358 | if __name__ == '__main__':
359 | main()
--------------------------------------------------------------------------------
/src/readmex/config/dependency_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "languages": {
3 | "python": {
4 | "dependency_files": ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"],
5 | "file_extensions": ["*.py"],
6 | "import_patterns": [
7 | {
8 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)",
9 | "format": "import {module}",
10 | "group": 1
11 | },
12 | {
13 | "pattern": "^from\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\s+import\\s+(.+)",
14 | "format": "from {module} import {items}",
15 | "group": 1,
16 | "items_group": 2
17 | }
18 | ],
19 | "prompt_template": "Based on the following import statements from a Python project, generate a requirements.txt file with appropriate package versions.\n\nImport statements found:\n{imports}\n\nExisting requirements.txt (if any):\n{existing}\n\nPlease generate a complete requirements.txt file that includes:\n1. Only external packages (not built-in Python modules)\n2. Reasonable version specifications (use >= for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing requirements if provided\n\nReturn only the requirements.txt content, one package per line in format: package>=version"
20 | },
21 | "javascript": {
22 | "dependency_files": ["package.json", "yarn.lock", "package-lock.json"],
23 | "file_extensions": ["*.js", "*.jsx", "*.ts", "*.tsx"],
24 | "import_patterns": [
25 | {
26 | "pattern": "^import\\s+.*?from\\s+['\"]([^'\"]+)['\"]",
27 | "format": "import from '{module}'",
28 | "group": 1
29 | },
30 | {
31 | "pattern": "^const\\s+.*?=\\s+require\\(['\"]([^'\"]+)['\"]\\)",
32 | "format": "require('{module}')",
33 | "group": 1
34 | },
35 | {
36 | "pattern": "^import\\s+['\"]([^'\"]+)['\"]",
37 | "format": "import '{module}'",
38 | "group": 1
39 | }
40 | ],
41 | "prompt_template": "Based on the following import statements from a JavaScript/TypeScript project, generate a package.json dependencies section.\n\nImport statements found:\n{imports}\n\nExisting package.json (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not relative imports)\n2. Reasonable version specifications (use ^ for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the dependencies object in JSON format."
42 | },
43 | "java": {
44 | "dependency_files": ["pom.xml", "build.gradle", "gradle.properties"],
45 | "file_extensions": ["*.java"],
46 | "import_patterns": [
47 | {
48 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);",
49 | "format": "import {module}",
50 | "group": 1
51 | },
52 | {
53 | "pattern": "^import\\s+static\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);",
54 | "format": "import static {module}",
55 | "group": 1
56 | }
57 | ],
58 | "prompt_template": "Based on the following import statements from a Java project, generate Maven dependencies or Gradle dependencies.\n\nImport statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not java.* or javax.* built-in packages)\n2. Common libraries with their typical versions\n3. Merge with existing dependencies if provided\n\nReturn the dependencies in Maven XML format or Gradle format as appropriate."
59 | },
60 | "go": {
61 | "dependency_files": ["go.mod", "go.sum"],
62 | "file_extensions": ["*.go"],
63 | "import_patterns": [
64 | {
65 | "pattern": "^import\\s+\"([^\"]+)\"",
66 | "format": "import \"{module}\"",
67 | "group": 1
68 | },
69 | {
70 | "pattern": "^import\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s+\"([^\"]+)\"",
71 | "format": "import \"{module}\"",
72 | "group": 1
73 | }
74 | ],
75 | "prompt_template": "Based on the following import statements from a Go project, generate a go.mod file.\n\nImport statements found:\n{imports}\n\nExisting go.mod (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not standard library)\n2. Reasonable version specifications\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the go.mod content."
76 | },
77 | "rust": {
78 | "dependency_files": ["Cargo.toml", "Cargo.lock"],
79 | "file_extensions": ["*.rs"],
80 | "import_patterns": [
81 | {
82 | "pattern": "^use\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:::[a-zA-Z_][a-zA-Z0-9_]*)*)",
83 | "format": "use {module}",
84 | "group": 1
85 | },
86 | {
87 | "pattern": "^extern\\s+crate\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
88 | "format": "extern crate {module}",
89 | "group": 1
90 | }
91 | ],
92 | "prompt_template": "Based on the following use statements from a Rust project, generate a Cargo.toml dependencies section.\n\nUse statements found:\n{imports}\n\nExisting Cargo.toml (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external crates (not std library)\n2. Reasonable version specifications\n3. Common crates with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the [dependencies] section of Cargo.toml."
93 | },
94 | "csharp": {
95 | "dependency_files": ["*.csproj", "packages.config", "*.sln"],
96 | "file_extensions": ["*.cs"],
97 | "import_patterns": [
98 | {
99 | "pattern": "^using\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*);",
100 | "format": "using {module}",
101 | "group": 1
102 | }
103 | ],
104 | "prompt_template": "Based on the following using statements from a C# project, generate NuGet package references.\n\nUsing statements found:\n{imports}\n\nExisting project file (if any):\n{existing}\n\nPlease generate package references that include:\n1. Only external packages (not System.* built-in namespaces)\n2. Reasonable version specifications\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn the PackageReference elements in XML format."
105 | },
106 | "php": {
107 | "dependency_files": ["composer.json", "composer.lock"],
108 | "file_extensions": ["*.php"],
109 | "import_patterns": [
110 | {
111 | "pattern": "^use\\s+([a-zA-Z_\\\\][a-zA-Z0-9_\\\\]*);",
112 | "format": "use {module}",
113 | "group": 1
114 | },
115 | {
116 | "pattern": "^require_once\\s+['\"]([^'\"]+)['\"]",
117 | "format": "require_once '{module}'",
118 | "group": 1
119 | },
120 | {
121 | "pattern": "^include_once\\s+['\"]([^'\"]+)['\"]",
122 | "format": "include_once '{module}'",
123 | "group": 1
124 | }
125 | ],
126 | "prompt_template": "Based on the following use/require statements from a PHP project, generate a composer.json dependencies section.\n\nStatements found:\n{imports}\n\nExisting composer.json (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external packages (not built-in PHP classes)\n2. Reasonable version specifications (use ^ for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the require section in JSON format."
127 | },
128 | "ruby": {
129 | "dependency_files": ["Gemfile", "Gemfile.lock", "*.gemspec"],
130 | "file_extensions": ["*.rb"],
131 | "import_patterns": [
132 | {
133 | "pattern": "^require\\s+['\"]([^'\"]+)['\"]",
134 | "format": "require '{module}'",
135 | "group": 1
136 | },
137 | {
138 | "pattern": "^require_relative\\s+['\"]([^'\"]+)['\"]",
139 | "format": "require_relative '{module}'",
140 | "group": 1
141 | }
142 | ],
143 | "prompt_template": "Based on the following require statements from a Ruby project, generate a Gemfile.\n\nRequire statements found:\n{imports}\n\nExisting Gemfile (if any):\n{existing}\n\nPlease generate gem dependencies that include:\n1. Only external gems (not built-in Ruby libraries)\n2. Reasonable version specifications\n3. Common gems with their typical versions\n4. Merge with existing dependencies if provided\n\nReturn only the Gemfile content."
144 | },
145 | "matlab": {
146 | "dependency_files": ["requirements.txt", "toolbox_dependencies.txt", "*.prj"],
147 | "file_extensions": ["*.m", "*.mlx", "*.mlapp"],
148 | "import_patterns": [
149 | {
150 | "pattern": "^addpath\\(['\"]([^'\"]+)['\"]\\)",
151 | "format": "addpath('{module}')",
152 | "group": 1
153 | },
154 | {
155 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)",
156 | "format": "import {module}",
157 | "group": 1
158 | },
159 | {
160 | "pattern": "^toolbox\\s*\\(['\"]([^'\"]+)['\"]\\)",
161 | "format": "toolbox('{module}')",
162 | "group": 1
163 | }
164 | ],
165 | "prompt_template": "Based on the following import/addpath statements from a MATLAB project, generate a toolbox dependencies file.\n\nStatements found:\n{imports}\n\nExisting dependencies (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external toolboxes and packages (not built-in MATLAB functions)\n2. Common MATLAB toolboxes with their names\n3. Third-party packages from File Exchange or GitHub\n4. Merge with existing dependencies if provided\n\nReturn only the dependencies list, one per line."
166 | },
167 | "c": {
168 | "dependency_files": ["Makefile", "CMakeLists.txt", "configure.ac", "*.pc"],
169 | "file_extensions": ["*.c", "*.h"],
170 | "import_patterns": [
171 | {
172 | "pattern": "^#include\\s*<([^>]+)>",
173 | "format": "#include <{module}>",
174 | "group": 1
175 | },
176 | {
177 | "pattern": "^#include\\s*['\"]([^'\"]+)['\"]",
178 | "format": "#include \"{module}\"",
179 | "group": 1
180 | }
181 | ],
182 | "prompt_template": "Based on the following #include statements from a C project, generate build dependencies.\n\nInclude statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external libraries (not standard C library headers)\n2. Common C libraries like OpenSSL, zlib, curl, etc.\n3. System libraries that need to be linked\n4. Merge with existing dependencies if provided\n\nReturn the dependencies in Makefile format or CMake format as appropriate."
183 | },
184 | "cpp": {
185 | "dependency_files": ["Makefile", "CMakeLists.txt", "conanfile.txt", "vcpkg.json"],
186 | "file_extensions": ["*.cpp", "*.cxx", "*.cc", "*.hpp", "*.hxx", "*.h"],
187 | "import_patterns": [
188 | {
189 | "pattern": "^#include\\s*<([^>]+)>",
190 | "format": "#include <{module}>",
191 | "group": 1
192 | },
193 | {
194 | "pattern": "^#include\\s*['\"]([^'\"]+)['\"]",
195 | "format": "#include \"{module}\"",
196 | "group": 1
197 | }
198 | ],
199 | "prompt_template": "Based on the following #include statements from a C++ project, generate build dependencies.\n\nInclude statements found:\n{imports}\n\nExisting build file (if any):\n{existing}\n\nPlease generate dependencies that include:\n1. Only external libraries (not standard C++ library headers)\n2. Common C++ libraries like Boost, Qt, OpenCV, etc.\n3. System libraries that need to be linked\n4. Merge with existing dependencies if provided\n\nReturn the dependencies in CMake format or Conan format as appropriate."
200 | }
201 | },
202 | "default_language": "python",
203 | "builtin_modules": {
204 | "python": [
205 | "os", "sys", "re", "json", "urllib", "http", "datetime", "time", "math",
206 | "random", "collections", "itertools", "functools", "operator", "pathlib",
207 | "typing", "abc", "contextlib", "copy", "pickle", "sqlite3", "csv",
208 | "configparser", "logging", "unittest", "threading", "multiprocessing",
209 | "asyncio", "socket", "email", "html", "xml", "base64", "hashlib",
210 | "hmac", "secrets", "uuid", "decimal", "fractions", "statistics",
211 | "io", "tempfile", "shutil", "glob", "fnmatch", "linecache", "fileinput"
212 | ],
213 | "javascript": [
214 | "fs", "path", "os", "crypto", "util", "events", "stream", "buffer",
215 | "url", "querystring", "http", "https", "net", "dns", "cluster",
216 | "child_process", "readline", "repl", "vm", "assert", "console"
217 | ],
218 | "java": [
219 | "java.lang", "java.util", "java.io", "java.net", "java.nio", "java.text",
220 | "java.time", "java.math", "java.security", "java.sql", "javax.sql",
221 | "javax.swing", "javax.awt", "javax.crypto", "javax.xml"
222 | ],
223 | "go": [
224 | "fmt", "os", "io", "net", "http", "time", "strings", "strconv", "math",
225 | "sort", "sync", "context", "encoding", "crypto", "database", "log",
226 | "flag", "path", "regexp", "runtime", "testing", "unsafe", "reflect"
227 | ],
228 | "rust": [
229 | "std", "core", "alloc", "proc_macro", "test"
230 | ],
231 | "csharp": [
232 | "System", "System.Collections", "System.IO", "System.Net", "System.Text",
233 | "System.Threading", "System.Linq", "System.Data", "System.Xml",
234 | "System.Drawing", "System.Windows", "Microsoft"
235 | ],
236 | "php": [
237 | "DateTime", "PDO", "Exception", "stdClass", "Closure", "Generator",
238 | "ReflectionClass", "SplFileInfo", "ArrayIterator", "RecursiveDirectoryIterator"
239 | ],
240 | "ruby": [
241 | "json", "yaml", "csv", "uri", "net", "open", "time", "date", "digest",
242 | "base64", "fileutils", "pathname", "tempfile", "logger", "benchmark"
243 | ],
244 | "matlab": [
245 | "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "exp", "floor",
246 | "log", "log10", "max", "min", "mod", "rand", "randn", "round", "sin",
247 | "sqrt", "tan", "zeros", "ones", "eye", "size", "length", "numel",
248 | "find", "sort", "unique", "sum", "mean", "std", "var", "plot", "figure",
249 | "subplot", "xlabel", "ylabel", "title", "legend", "grid", "hold",
250 | "disp", "fprintf", "sprintf", "input", "load", "save", "clear", "clc",
251 | "who", "whos", "exist", "which", "path", "cd", "pwd", "dir", "ls",
252 | "mkdir", "rmdir", "delete", "copyfile", "movefile", "isdir", "isfile"
253 | ],
254 | "c": [
255 | "stdio.h", "stdlib.h", "string.h", "math.h", "time.h", "ctype.h",
256 | "assert.h", "errno.h", "float.h", "limits.h", "locale.h", "setjmp.h",
257 | "signal.h", "stdarg.h", "stddef.h", "unistd.h", "sys/types.h",
258 | "sys/stat.h", "fcntl.h", "dirent.h", "pthread.h"
259 | ],
260 | "cpp": [
261 | "iostream", "vector", "string", "algorithm", "memory", "utility",
262 | "functional", "iterator", "numeric", "map", "set", "unordered_map",
263 | "unordered_set", "queue", "stack", "deque", "list", "array", "tuple",
264 | "chrono", "thread", "mutex", "condition_variable", "future", "atomic",
265 | "regex", "random", "typeinfo", "exception", "stdexcept", "cassert",
266 | "cmath", "cstdlib", "cstring", "ctime", "cctype", "climits", "cfloat"
267 | ]
268 | }
269 | }
--------------------------------------------------------------------------------
/src/readmex/utils/language_analyzer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | from collections import defaultdict
4 | from pathlib import Path
5 | from typing import Dict, List, Any, Optional
6 | import fnmatch
7 |
8 |
9 | class LanguageAnalyzer:
10 | """Project Language Analyzer - Detect programming language distribution in projects"""
11 |
12 | def __init__(self, config_path: Optional[str] = None):
13 | """
14 | Initialize language analyzer
15 |
16 | Args:
17 | config_path: Language mapping config file path, if None use default path
18 | """
19 | self.language_mapping = self._load_language_mapping(config_path)
20 | self.extension_to_language = self._build_extension_mapping()
21 |
22 | # Load ignore patterns
23 | self.ignore_dirs, self.ignore_files = self._load_ignore_patterns()
24 |
25 | def _load_language_mapping(self, config_path: Optional[str] = None) -> Dict[str, List[str]]:
26 | """Load language mapping config"""
27 | if config_path is None:
28 | # Use default path
29 | current_dir = Path(__file__).parent.parent
30 | config_path = current_dir / "config" / "language_mapping.json"
31 |
32 | try:
33 | with open(config_path, 'r', encoding='utf-8') as f:
34 | return json.load(f)
35 | except FileNotFoundError:
36 | print(f"Warning: Language mapping config file {config_path} not found, using default config")
37 | return self._get_default_mapping()
38 | except json.JSONDecodeError as e:
39 | print(f"Warning: Language mapping config file format error: {e}, using default config")
40 | return self._get_default_mapping()
41 |
42 | def _get_default_mapping(self) -> Dict[str, List[str]]:
43 | """Get default language mapping"""
44 | return {
45 | "Python": [".py", ".pyx", ".pyi", ".pyw"],
46 | "JavaScript": [".js", ".jsx", ".mjs"],
47 | "TypeScript": [".ts", ".tsx"],
48 | "Java": [".java", ".class", ".jar"],
49 | "C": [".c"],
50 | "C++": [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h++"],
51 | "C/C++": [".h"],
52 | "Go": [".go"],
53 | "Rust": [".rs"],
54 | "PHP": [".php", ".phtml"],
55 | "Ruby": [".rb", ".erb", ".rake"],
56 | "HTML": [".html", ".htm"],
57 | "CSS": [".css"],
58 | "JSON": [".json"],
59 | "Markdown": [".md", ".markdown"],
60 | "Text": [".txt"],
61 | "Shell": [".sh", ".bash", ".zsh", ".fish"],
62 | "SQL": [".sql"],
63 | "YAML": [".yaml", ".yml"],
64 | "XML": [".xml"]
65 | }
66 |
67 | def _build_extension_mapping(self) -> Dict[str, str]:
68 | """Build extension to language mapping"""
69 | extension_map = {}
70 | for language, extensions in self.language_mapping.items():
71 | for ext in extensions:
72 | # Handle special filenames (e.g. Dockerfile, Makefile)
73 | if not ext.startswith('.'):
74 | extension_map[ext] = language
75 | else:
76 | extension_map[ext.lower()] = language
77 | return extension_map
78 |
79 | def _load_ignore_patterns(self) -> tuple[set, set]:
80 | """Load ignore patterns config"""
81 | # Use default path
82 | current_dir = Path(__file__).parent.parent
83 | config_path = current_dir / "config" / "ignore_patterns.json"
84 |
85 | try:
86 | with open(config_path, 'r', encoding='utf-8') as f:
87 | config = json.load(f)
88 | ignore_dirs = set(config.get('ignore_dirs', []))
89 | ignore_files = set(config.get('ignore_files', []))
90 | return ignore_dirs, ignore_files
91 | except FileNotFoundError:
92 | print(f"Warning: Ignore patterns config file {config_path} not found, using default config")
93 | return self._get_default_ignore_patterns()
94 | except json.JSONDecodeError as e:
95 | print(f"Warning: Ignore patterns config file format error: {e}, using default config")
96 | return self._get_default_ignore_patterns()
97 |
98 | def _get_default_ignore_patterns(self) -> tuple[set, set]:
99 | """Get default ignore patterns"""
100 | ignore_dirs = {
101 | '.git', '__pycache__', 'node_modules', '.venv', 'venv',
102 | 'env', '.pytest_cache', '.idea', '.vscode', 'build',
103 | 'dist', 'target', 'bin', 'obj', '.DS_Store', '.mypy_cache',
104 | 'coverage', '.coverage', 'htmlcov', '.tox', '.eggs',
105 | '*.egg-info', '*.egg', '.pytest_cache', '.cache',
106 | '.next', '.nuxt', 'out', '.output', '.vercel'
107 | }
108 |
109 | ignore_files = {
110 | '.DS_Store', '.gitignore', '.gitattributes', '.editorconfig',
111 | 'Thumbs.db', 'desktop.ini', '.env', '.env.local', '.env.production'
112 | }
113 |
114 | return ignore_dirs, ignore_files
115 |
116 | def analyze_project(self, project_path: str) -> Dict[str, Any]:
117 | """
118 | Analyze language distribution in the project
119 |
120 | Args:
121 | project_path: Project path
122 |
123 | Returns:
124 | Dictionary containing language analysis results
125 | """
126 | project_path = Path(project_path)
127 | if not project_path.exists():
128 | raise FileNotFoundError(f"Project path does not exist: {project_path}")
129 |
130 | language_stats = defaultdict(lambda: {'files': 0, 'lines': 0, 'bytes': 0})
131 |
132 | # Traverse all files in the project
133 | for file_path in project_path.rglob('*'):
134 | if file_path.is_file():
135 | # Check if the file should be ignored
136 | if self._should_ignore(file_path):
137 | continue
138 |
139 | # Determine language type
140 | language = self._get_language(file_path)
141 |
142 | if language:
143 | # Count file info
144 | try:
145 | file_size = file_path.stat().st_size
146 | line_count = self._count_lines(file_path)
147 |
148 | language_stats[language]['files'] += 1
149 | language_stats[language]['lines'] += line_count
150 | language_stats[language]['bytes'] += file_size
151 |
152 | except (OSError, UnicodeDecodeError):
153 | # Skip unreadable files
154 | continue
155 |
156 | return self._calculate_percentages(language_stats)
157 |
158 | def _should_ignore(self, file_path: Path) -> bool:
159 | """Check if the file or directory should be ignored"""
160 | # Check directory names
161 | for part in file_path.parts:
162 | if part in self.ignore_dirs:
163 | return True
164 | # Check wildcard patterns
165 | for pattern in self.ignore_dirs:
166 | if '*' in pattern and fnmatch.fnmatch(part, pattern):
167 | return True
168 |
169 | # Check file names
170 | if file_path.name in self.ignore_files:
171 | return True
172 |
173 | return False
174 |
175 | def _get_language(self, file_path: Path) -> Optional[str]:
176 | """Determine language type by file extension and content"""
177 | # Check special filenames (e.g. Dockerfile, Makefile)
178 | if file_path.name in self.extension_to_language:
179 | return self.extension_to_language[file_path.name]
180 |
181 | # Check file extension
182 | extension = file_path.suffix.lower()
183 | if extension in self.extension_to_language:
184 | return self.extension_to_language[extension]
185 |
186 | # Check shebang line for script language
187 | try:
188 | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
189 | first_line = f.readline().strip()
190 | if first_line.startswith('#!'):
191 | return self._detect_shebang_language(first_line)
192 | except:
193 | pass
194 |
195 | return None
196 |
197 | def _detect_shebang_language(self, shebang_line: str) -> str:
198 | """Detect language by shebang line"""
199 | shebang = shebang_line.lower()
200 |
201 | if 'python' in shebang:
202 | return 'Python'
203 | elif 'bash' in shebang or 'sh' in shebang:
204 | return 'Shell'
205 | elif 'node' in shebang:
206 | return 'JavaScript'
207 | elif 'ruby' in shebang:
208 | return 'Ruby'
209 | elif 'perl' in shebang:
210 | return 'Perl'
211 | elif 'php' in shebang:
212 | return 'PHP'
213 |
214 | return 'Shell' # Default to Shell
215 |
216 | def _count_lines(self, file_path: Path) -> int:
217 | """Count file lines"""
218 | try:
219 | with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
220 | return sum(1 for _ in f)
221 | except:
222 | return 0
223 |
224 | def _calculate_percentages(self, language_stats: Dict) -> Dict[str, Any]:
225 | """Calculate percentages for each language"""
226 | total_files = sum(stats['files'] for stats in language_stats.values())
227 | total_lines = sum(stats['lines'] for stats in language_stats.values())
228 | total_bytes = sum(stats['bytes'] for stats in language_stats.values())
229 |
230 | results = []
231 | for language, stats in language_stats.items():
232 | if total_files > 0:
233 | file_percentage = (stats['files'] / total_files) * 100
234 | else:
235 | file_percentage = 0
236 |
237 | if total_lines > 0:
238 | line_percentage = (stats['lines'] / total_lines) * 100
239 | else:
240 | line_percentage = 0
241 |
242 | if total_bytes > 0:
243 | byte_percentage = (stats['bytes'] / total_bytes) * 100
244 | else:
245 | byte_percentage = 0
246 |
247 | results.append({
248 | 'language': language,
249 | 'files': stats['files'],
250 | 'lines': stats['lines'],
251 | 'bytes': stats['bytes'],
252 | 'file_percentage': round(file_percentage, 2),
253 | 'line_percentage': round(line_percentage, 2),
254 | 'byte_percentage': round(byte_percentage, 2)
255 | })
256 |
257 | # Sort by lines of code
258 | results.sort(key=lambda x: x['lines'], reverse=True)
259 |
260 | return {
261 | 'summary': {
262 | 'total_files': total_files,
263 | 'total_lines': total_lines,
264 | 'total_bytes': total_bytes,
265 | 'total_languages': len(results)
266 | },
267 | 'languages': results
268 | }
269 |
270 | def get_primary_language(self, project_path: str) -> Optional[str]:
271 | """
272 | Get the primary programming language of the project
273 |
274 | Args:
275 | project_path: Project path
276 |
277 | Returns:
278 | Primary programming language name, or None if not found
279 | """
280 | results = self.analyze_project(project_path)
281 | if results['languages']:
282 | return results['languages'][0]['language']
283 | return None
284 |
285 | def get_language_summary(self, project_path: str) -> str:
286 | """
287 | Get a text summary of the project's language distribution
288 |
289 | Args:
290 | project_path: Project path
291 |
292 | Returns:
293 | Language distribution summary text
294 | """
295 | results = self.analyze_project(project_path)
296 |
297 | if not results['languages']:
298 | return "No programming language files detected."
299 |
300 | summary_lines = []
301 | summary_lines.append(f"Project contains {results['summary']['total_languages']} programming languages")
302 | summary_lines.append(f"Total files: {results['summary']['total_files']}")
303 | summary_lines.append(f"Total lines of code: {results['summary']['total_lines']}")
304 |
305 | # Show top 3 main languages
306 | top_languages = results['languages'][:3]
307 | summary_lines.append("\nMain languages:")
308 | for lang in top_languages:
309 | summary_lines.append(
310 | f" {lang['language']}: {lang['line_percentage']:.1f}% "
311 | f"({lang['lines']} lines, {lang['files']} files)"
312 | )
313 |
314 | return "\n".join(summary_lines)
315 |
316 | def save_analysis_result(self, project_path: str, output_path: str) -> None:
317 | """
318 | Save language analysis result to file
319 |
320 | Args:
321 | project_path: Project path
322 | output_path: Output file path
323 | """
324 | results = self.analyze_project(project_path)
325 |
326 | with open(output_path, 'w', encoding='utf-8') as f:
327 | json.dump(results, f, ensure_ascii=False, indent=2)
328 |
329 |
330 | def analyze_project_languages(project_path: str, config_path: Optional[str] = None) -> Dict[str, Any]:
331 | """
332 | Convenience function: analyze project language distribution
333 |
334 | Args:
335 | project_path: Project path
336 | config_path: Language mapping config file path
337 |
338 | Returns:
339 | Language analysis result
340 | """
341 | analyzer = LanguageAnalyzer(config_path)
342 | return analyzer.analyze_project(project_path)
343 |
344 | if __name__ == "__main__":
345 | """Test when running this script directly"""
346 | import sys
347 | from pathlib import Path
348 |
349 | # Get the project root directory as the test path
350 | current_dir = Path(__file__).parent.parent.parent
351 |
352 | print("🔍 Language Analyzer Test")
353 | print("=" * 50)
354 | print(f"Test project path: {current_dir}")
355 | print()
356 |
357 | try:
358 | # Create analyzer instance
359 | analyzer = LanguageAnalyzer()
360 |
361 | # Analyze project
362 | print("Analyzing project...")
363 | results = analyzer.analyze_project(str(current_dir))
364 |
365 | # Show results
366 | print("\n📊 Analysis Result:")
367 | print(f" Total files: {results['summary']['total_files']}")
368 | print(f" Total languages: {results['summary']['total_languages']}")
369 | print(f" Total lines of code: {results['summary']['total_lines']}")
370 | print(f" Total file size: {results['summary']['total_bytes']} bytes")
371 |
372 | if results['languages']:
373 | print("\n📈 Language Distribution:")
374 | print("-" * 60)
375 | print(f"{'Language':<15} {'Files':<8} {'Lines':<10} {'File %':<10} {'Line %':<10}")
376 | print("-" * 60)
377 |
378 | for lang in results['languages'][:10]: # Show top 10 languages
379 | print(f"{lang['language']:<15} {lang['files']:<8} {lang['lines']:<10} "
380 | f"{lang['file_percentage']:<10.1f}% {lang['line_percentage']:<10.1f}%")
381 |
382 | print("-" * 60)
383 |
384 | # Get primary language
385 | primary_lang = analyzer.get_primary_language(str(current_dir))
386 | print(f"\n🏆 Primary Language: {primary_lang}")
387 |
388 | # Get language summary
389 | summary = analyzer.get_language_summary(str(current_dir))
390 | print(f"\n📋 Language Summary:\n{summary}")
391 |
392 | else:
393 | print("\n⚠️ No programming language files detected.")
394 |
395 | print("\n✅ Test completed!")
396 |
397 | except FileNotFoundError as e:
398 | print(f"❌ Error: Project path does not exist - {e}")
399 | sys.exit(1)
400 | except Exception as e:
401 | print(f"❌ Error: {e}")
402 | sys.exit(1)
403 |
--------------------------------------------------------------------------------
/src/readmex/utils/dependency_analyzer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import json
4 | from typing import Set, List, Dict, Any
5 | from pathlib import Path
6 | from rich.console import Console
7 | from readmex.utils.file_handler import find_files, load_gitignore_patterns
8 | from readmex.config import DEFAULT_IGNORE_PATTERNS
9 | from readmex.utils.model_client import ModelClient
10 |
11 |
12 | class DependencyAnalyzer:
13 | """Multi-language project dependency analyzer class"""
14 |
15 | def __init__(self, project_dir: str, primary_language: str = "python", model_client=None, console=None):
16 | """
17 | Initialize dependency analyzer
18 |
19 | Args:
20 | project_dir: Project root directory path
21 | primary_language: Primary programming language of the project
22 | model_client: Model client for generating dependency files
23 | console: Rich console object for output
24 | """
25 | self.project_dir = project_dir
26 | # 安全处理可能为 None 的 primary_language 参数
27 | self.primary_language = (primary_language or "python").lower()
28 | self.model_client = model_client
29 | self.console = console or Console()
30 |
31 | # Load dependency configuration
32 | self.config = self._load_dependency_config()
33 |
34 | # Validate primary language
35 | if self.primary_language not in self.config["languages"]:
36 | self.console.print(f"[yellow]Warning: Language '{self.primary_language}' not supported, falling back to default[/yellow]")
37 | self.primary_language = self.config["default_language"]
38 |
39 | def _load_dependency_config(self) -> Dict[str, Any]:
40 | """Load dependency configuration from JSON file"""
41 | try:
42 | config_path = Path(__file__).parent.parent / "config" / "dependency_config.json"
43 | with open(config_path, 'r', encoding='utf-8') as f:
44 | return json.load(f)
45 | except FileNotFoundError:
46 | self.console.print("[red]Warning: dependency_config.json not found, using default Python configuration[/red]")
47 | return self._get_default_config()
48 | except json.JSONDecodeError as e:
49 | self.console.print(f"[red]Warning: Invalid JSON in dependency_config.json: {e}[/red]")
50 | return self._get_default_config()
51 |
52 | def _get_default_config(self) -> Dict[str, Any]:
53 | """Get default configuration for Python"""
54 | return {
55 | "languages": {
56 | "python": {
57 | "dependency_files": ["requirements.txt"],
58 | "file_extensions": ["*.py"],
59 | "import_patterns": [
60 | {
61 | "pattern": "^import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)",
62 | "format": "import {module}",
63 | "group": 1
64 | },
65 | {
66 | "pattern": "^from\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\s+import\\s+(.+)",
67 | "format": "from {module} import {items}",
68 | "group": 1,
69 | "items_group": 2
70 | }
71 | ],
72 | "prompt_template": "Based on the following import statements from a Python project, generate a requirements.txt file with appropriate package versions.\n\nImport statements found:\n{imports}\n\nExisting requirements.txt (if any):\n{existing}\n\nPlease generate a complete requirements.txt file that includes:\n1. Only external packages (not built-in Python modules)\n2. Reasonable version specifications (use >= for flexibility)\n3. Common packages with their typical versions\n4. Merge with existing requirements if provided\n\nReturn only the requirements.txt content, one package per line in format: package>=version"
73 | }
74 | },
75 | "default_language": "python",
76 | "builtin_modules": {
77 | "python": ["os", "sys", "re", "json", "urllib", "http", "datetime", "time", "math", "random", "collections", "itertools", "functools", "operator", "pathlib", "typing", "abc", "contextlib", "copy", "pickle", "sqlite3", "csv", "configparser", "logging", "unittest", "threading", "multiprocessing", "asyncio", "socket", "email", "html", "xml", "base64", "hashlib", "hmac", "secrets", "uuid", "decimal", "fractions", "statistics", "io", "tempfile", "shutil", "glob", "fnmatch", "linecache", "fileinput"]
78 | }
79 | }
80 |
81 | def analyze_project_dependencies(self, output_dir: str = None) -> str:
82 | """
83 | Analyze project dependencies and generate dependency files
84 |
85 | Args:
86 | output_dir: Output directory, saves files if provided
87 |
88 | Returns:
89 | Generated dependency file content
90 | """
91 | lang_config = self.config["languages"][self.primary_language]
92 | self.console.print(f"[cyan]🤖 Generating {self.primary_language} project dependencies...[/cyan]")
93 |
94 | # Check if existing dependency files exist
95 | existing_dependencies = self._get_existing_dependencies()
96 | if existing_dependencies:
97 | self.console.print(f"[yellow]Found existing dependency files[/yellow]")
98 |
99 | # Scan all source files to extract import statements
100 | gitignore_patterns = load_gitignore_patterns(self.project_dir)
101 | ignore_patterns = DEFAULT_IGNORE_PATTERNS + gitignore_patterns
102 | source_files = list(find_files(self.project_dir, lang_config["file_extensions"], ignore_patterns))
103 |
104 | all_imports = set()
105 |
106 | if source_files:
107 | self.console.print(f"Scanning {len(source_files)} {self.primary_language} files for imports...")
108 |
109 | for source_file in source_files:
110 | try:
111 | with open(source_file, "r", encoding="utf-8") as f:
112 | content = f.read()
113 |
114 | # Extract import statements using language-specific patterns
115 | import_lines = self._extract_imports_by_language(content)
116 | all_imports.update(import_lines)
117 |
118 | except Exception as e:
119 | self.console.print(
120 | f"[yellow]Warning: Could not read {source_file}: {e}[/yellow]"
121 | )
122 |
123 | if all_imports:
124 | self.console.print(f"Found {len(all_imports)} unique import statements")
125 |
126 | # Filter out built-in modules
127 | external_imports = self._filter_external_imports(all_imports)
128 |
129 | if external_imports:
130 | # Use LLM to generate dependency file
131 | imports_text = "\n".join(sorted(external_imports))
132 | prompt = lang_config["prompt_template"].format(
133 | imports=imports_text,
134 | existing=existing_dependencies
135 | )
136 |
137 | self.console.print(f"Generating {self.primary_language} dependency file...")
138 | generated_dependencies = self.model_client.get_answer(prompt)
139 |
140 | # Clean the generated content
141 | generated_dependencies = self._clean_dependency_content(
142 | generated_dependencies
143 | )
144 | else:
145 | generated_dependencies = f"# No external {self.primary_language} dependencies found\n"
146 | if existing_dependencies:
147 | generated_dependencies = existing_dependencies
148 | else:
149 | generated_dependencies = f"# No {self.primary_language} import statements found\n"
150 | if existing_dependencies:
151 | generated_dependencies = existing_dependencies
152 | else:
153 | generated_dependencies = f"# No {self.primary_language} files found\n"
154 | if existing_dependencies:
155 | generated_dependencies = existing_dependencies
156 |
157 | self.console.print(f"Generated dependencies: {generated_dependencies}")
158 | # Save generated dependency files to output folder
159 | if output_dir:
160 | self._save_dependency_files(
161 | output_dir, generated_dependencies, existing_dependencies, all_imports
162 | )
163 |
164 | self.console.print(f"[green]✔ {self.primary_language} project dependencies generated.[/green]")
165 | return generated_dependencies
166 |
167 | def _get_existing_dependencies(self) -> str:
168 | """Get existing dependency file content for the current language"""
169 | lang_config = self.config["languages"][self.primary_language]
170 | existing_content = ""
171 |
172 | for dep_file in lang_config["dependency_files"]:
173 | dep_path = os.path.join(self.project_dir, dep_file)
174 | if os.path.exists(dep_path):
175 | try:
176 | with open(dep_path, "r", encoding="utf-8") as f:
177 | content = f.read()
178 | if content.strip():
179 | existing_content += f"\n=== {dep_file} ===\n{content}\n"
180 | except Exception as e:
181 | self.console.print(f"[yellow]Warning: Could not read {dep_file}: {e}[/yellow]")
182 |
183 | return existing_content.strip()
184 |
185 | def _extract_imports_by_language(self, content: str) -> Set[str]:
186 | """Extract import statements using language-specific patterns"""
187 | lang_config = self.config["languages"][self.primary_language]
188 | imports = set()
189 | lines = content.split("\n")
190 |
191 | for line in lines:
192 | line = line.strip()
193 |
194 | # Skip comment lines and empty lines
195 | if not line or line.startswith("#") or line.startswith("//") or line.startswith("/*"):
196 | continue
197 |
198 | # Apply language-specific patterns
199 | for pattern_config in lang_config["import_patterns"]:
200 | pattern = pattern_config["pattern"]
201 | match = re.match(pattern, line)
202 |
203 | if match:
204 | module = match.group(pattern_config["group"])
205 |
206 | # Format the import statement
207 | if "items_group" in pattern_config:
208 | items = match.group(pattern_config["items_group"])
209 | formatted = pattern_config["format"].format(module=module, items=items)
210 | else:
211 | formatted = pattern_config["format"].format(module=module)
212 |
213 | imports.add(formatted)
214 | break
215 |
216 | return imports
217 |
218 | def _filter_external_imports(self, imports: Set[str]) -> Set[str]:
219 | """Filter out built-in modules to keep only external dependencies"""
220 | builtin_modules = self.config.get("builtin_modules", {}).get(self.primary_language, [])
221 | external_imports = set()
222 |
223 | for import_stmt in imports:
224 | # Extract module name from import statement
225 | module_name = self._extract_module_name(import_stmt)
226 |
227 | # Check if it's a built-in module
228 | is_builtin = False
229 | for builtin in builtin_modules:
230 | if module_name.startswith(builtin):
231 | is_builtin = True
232 | break
233 |
234 | # Skip relative imports and local files
235 | if not is_builtin and not module_name.startswith('.') and '/' not in module_name:
236 | external_imports.add(import_stmt)
237 |
238 | return external_imports
239 |
240 | def _extract_module_name(self, import_stmt: str) -> str:
241 | """Extract the base module name from an import statement"""
242 | # Handle different import formats
243 | if import_stmt.startswith("import "):
244 | module = import_stmt[7:].split()[0]
245 | elif import_stmt.startswith("from "):
246 | module = import_stmt.split(" import ")[0][5:]
247 | elif "require(" in import_stmt:
248 | # JavaScript/Node.js require
249 | match = re.search(r"require\(['\"]([^'\"]+)['\"]", import_stmt)
250 | module = match.group(1) if match else ""
251 | elif import_stmt.startswith("use "):
252 | # Rust use statement
253 | module = import_stmt[4:].split("::")[0]
254 | elif import_stmt.startswith("using "):
255 | # C# using statement
256 | module = import_stmt[6:].rstrip(";")
257 | else:
258 | module = import_stmt
259 |
260 | # Get the top-level module name
261 | return module.split('.')[0].split('::')[0].split('/')[0]
262 |
263 | def _clean_dependency_content(self, content: str) -> str:
264 | """Clean generated dependency file content (language-agnostic)"""
265 | lines = content.split("\n")
266 | cleaned_lines = []
267 |
268 | for line in lines:
269 | line = line.strip()
270 |
271 | # Skip empty lines and obvious non-dependency format lines
272 | if not line or line.startswith("```") or line.startswith("Based on"):
273 | continue
274 |
275 | # Keep lines that look like dependency specifications
276 | if any(char in line for char in ["=", ">", "<", "~", "^"]) or line.startswith("#"):
277 | cleaned_lines.append(line)
278 | elif re.match(r"^[a-zA-Z0-9_-]+$", line):
279 | # If only package name, keep as is (version will be handled by LLM)
280 | cleaned_lines.append(line)
281 |
282 | return "\n".join(cleaned_lines)
283 |
284 | def _save_dependency_files(
285 | self,
286 | output_dir: str,
287 | generated_dependencies: str,
288 | existing_dependencies: str,
289 | all_imports: Set[str]
290 | ) -> None:
291 | """Save dependency files and analysis information"""
292 | lang_config = self.config["languages"][self.primary_language]
293 |
294 | # Determine output filename based on language
295 | primary_dep_file = lang_config["dependency_files"][0]
296 | output_dep_path = os.path.join(output_dir, primary_dep_file)
297 |
298 | # Save generated dependency file
299 | with open(output_dep_path, "w", encoding="utf-8") as f:
300 | f.write(generated_dependencies)
301 | self.console.print(
302 | f"[green]✔ Generated {primary_dep_file} saved to: {output_dep_path}[/green]"
303 | )
304 |
305 | # Save dependency analysis information
306 | dependencies_info = f"""# {self.primary_language.title()} Dependencies Analysis Report
307 |
308 | ## Language: {self.primary_language}
309 | ## Primary dependency file: {primary_dep_file}
310 |
311 | ## Existing dependency files:
312 | {existing_dependencies if existing_dependencies else "None found"}
313 |
314 | ## Discovered imports ({len(all_imports)} unique):
315 | {chr(10).join(sorted(all_imports)) if all_imports else "No imports found"}
316 |
317 | ## Generated dependency file:
318 | {generated_dependencies}
319 | """
320 | dependencies_analysis_path = os.path.join(
321 | output_dir, f"{self.primary_language}_dependencies_analysis.txt"
322 | )
323 | with open(dependencies_analysis_path, "w", encoding="utf-8") as f:
324 | f.write(dependencies_info)
325 | self.console.print(
326 | f"[green]✔ Dependencies analysis saved to: {dependencies_analysis_path}[/green]"
327 | )
328 |
329 | # Backward compatibility methods (deprecated)
330 | def _extract_imports(self, content: str) -> Set[str]:
331 | """
332 | Legacy method for Python import extraction (deprecated)
333 | Use _extract_imports_by_language instead
334 | """
335 | return self._extract_imports_by_language(content)
336 |
337 | def _clean_requirements_content(self, content: str) -> str:
338 | """
339 | Legacy method for cleaning requirements (deprecated)
340 | Use _clean_dependency_content instead
341 | """
342 | return self._clean_dependency_content(content)
343 |
344 | def _save_requirements_files(self, output_dir: str, generated_requirements: str,
345 | existing_dependencies: str, all_imports: Set[str]) -> None:
346 | """
347 | Legacy method for saving requirements (deprecated)
348 | Use _save_dependency_files instead
349 | """
350 | self._save_dependency_files(output_dir, generated_requirements, existing_dependencies, all_imports)
351 |
352 | def get_project_imports(self) -> Set[str]:
353 | """
354 | Get all import statements in the project for the current language
355 |
356 | Returns:
357 | Set of all import statements
358 | """
359 | lang_config = self.config["languages"][self.primary_language]
360 | gitignore_patterns = load_gitignore_patterns(self.project_dir)
361 | ignore_patterns = DEFAULT_IGNORE_PATTERNS + gitignore_patterns
362 | source_files = list(find_files(self.project_dir, lang_config["file_extensions"], ignore_patterns))
363 |
364 | all_imports = set()
365 |
366 | for source_file in source_files:
367 | try:
368 | with open(source_file, "r", encoding="utf-8") as f:
369 | content = f.read()
370 | import_lines = self._extract_imports_by_language(content)
371 | all_imports.update(import_lines)
372 | except Exception:
373 | continue
374 |
375 | return all_imports
376 |
377 | def get_existing_requirements(self) -> str:
378 | """
379 | Get existing dependency file content for the current language
380 |
381 | Returns:
382 | Existing dependency file content, or empty string if not found
383 | """
384 | return self._get_existing_dependencies()
385 |
386 | def get_supported_languages(self) -> List[str]:
387 | """
388 | Get list of supported programming languages
389 |
390 | Returns:
391 | List of supported language names
392 | """
393 | return list(self.config["languages"].keys())
394 |
395 | def set_language(self, language: str) -> bool:
396 | """
397 | Change the primary language for dependency analysis
398 |
399 | Args:
400 | language: New primary language
401 |
402 | Returns:
403 | True if language was set successfully, False if not supported
404 | """
405 | # 安全处理可能为 None 的 language 参数
406 | if language is None:
407 | self.console.print(f"[red]Language cannot be None[/red]")
408 | return False
409 |
410 | language = language.lower()
411 | if language in self.config["languages"]:
412 | self.primary_language = language
413 | self.console.print(f"[green]Language changed to: {language}[/green]")
414 | return True
415 | else:
416 | self.console.print(f"[red]Language '{language}' not supported[/red]")
417 | return False
418 |
419 | if __name__ == "__main__":
420 | from pathlib import Path
421 |
422 | # Test the multi-language dependency analyzer
423 | output_dir = Path(__file__).parent.parent.parent.parent / "readmex_output"
424 | output_dir.mkdir(exist_ok=True)
425 |
426 | # Test with Python (default)
427 | print("Testing Python dependency analysis...")
428 | model_client = ModelClient()
429 | analyzer = DependencyAnalyzer(
430 | project_dir=".",
431 | primary_language="python",
432 | model_client=model_client,
433 | console=None
434 | )
435 |
436 | print(f"Supported languages: {analyzer.get_supported_languages()}")
437 | print(f"Current language: {analyzer.primary_language}")
438 |
439 | # Test import extraction
440 | imports = analyzer.get_project_imports()
441 | print(f"Found {len(imports)} import statements")
442 |
443 | # Test dependency analysis (commented out to avoid API calls)
444 | # analyzer.analyze_project_dependencies(output_dir=output_dir)
445 |
446 | # Test language switching
447 | print("\nTesting language switching...")
448 | analyzer.set_language("javascript")
449 | analyzer.set_language("unsupported_language")
450 | analyzer.set_language("python")
--------------------------------------------------------------------------------
/src/readmex/utils/model_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | from rich.console import Console
3 | import requests
4 | from openai import OpenAI, AzureOpenAI
5 | from typing import Optional, Dict, Union
6 | from readmex.config import get_llm_config, get_t2i_config, validate_config
7 | import time
8 |
9 |
10 | class ModelClient:
11 | """Model client class for LLM Q&A and text-to-image functionality"""
12 |
13 | def __init__(self, max_tokens: int = 10000, temperature: float = 0.7,
14 | image_size: str = "1024x1024", quality: str = "hd"):
15 | """
16 | Initialize model client
17 |
18 | Args:
19 | max_tokens: Maximum number of tokens
20 | temperature: Temperature parameter
21 | image_size: Image size
22 | quality: Image quality
23 | """
24 | # Validate configuration
25 | validate_config()
26 |
27 | # Get configurations
28 | self.llm_config = get_llm_config()
29 | self.t2i_config = get_t2i_config()
30 |
31 | # Set parameters
32 | self.max_tokens = max_tokens
33 | self.temperature = temperature
34 | self.image_size = image_size
35 | self.quality = quality
36 |
37 | # Initialize console
38 | self.console = Console()
39 |
40 | # Check if we're using Azure OpenAI with detailed logging
41 | self.console.print("[cyan]🔧 ModelClient Initialization[/cyan]")
42 | self.console.print(f"[dim]LLM Base URL: {self.llm_config['base_url']}[/dim]")
43 | self.console.print(f"[dim]T2I Base URL: {self.t2i_config['base_url']}[/dim]")
44 |
45 | self.is_llm_azure = self._is_azure_openai(self.llm_config["base_url"])
46 | self.is_t2i_azure = self._is_azure_openai(self.t2i_config["base_url"])
47 |
48 | # Log detection results
49 | llm_provider = "[blue]Azure OpenAI[/blue]" if self.is_llm_azure else "[green]Standard OpenAI[/green]"
50 | t2i_provider = "[blue]Azure OpenAI[/blue]" if self.is_t2i_azure else "[green]Standard OpenAI[/green]"
51 | self.console.print(f"[cyan]📊 LLM Provider:[/cyan] {llm_provider}")
52 | self.console.print(f"[cyan]🎨 T2I Provider:[/cyan] {t2i_provider}")
53 |
54 | # Initialize clients
55 | self.console.print("[cyan]🔌 Initializing clients...[/cyan]")
56 | self.llm_client = self._initialize_llm_client()
57 | self.t2i_client = self._initialize_t2i_client()
58 | self.console.print("[green]✓ ModelClient initialization complete[/green]")
59 |
60 | def _is_azure_openai(self, base_url: str) -> bool:
61 | """
62 | Check if the base URL is for Azure OpenAI
63 |
64 | Args:
65 | base_url: The base URL to check
66 |
67 | Returns:
68 | True if it's Azure OpenAI, False otherwise
69 | """
70 | is_azure = ".openai.azure.com" in base_url.lower()
71 | self.console.print(f"[dim]🔍 Checking URL: {base_url}[/dim]")
72 | self.console.print(f"[dim] Contains '.openai.azure.com': {is_azure}[/dim]")
73 | return is_azure
74 |
75 | def _extract_azure_info(self, base_url: str) -> tuple[str, str, str]:
76 | """
77 | Extract Azure OpenAI endpoint, deployment name and API version from base_url
78 |
79 | Args:
80 | base_url: Azure OpenAI URL like:
81 | https://resource.openai.azure.com/openai/deployments/model-name/...?api-version=2024-02-01
82 |
83 | Returns:
84 | Tuple of (azure_endpoint, deployment_name, api_version)
85 | """
86 | import re
87 | from urllib.parse import urlparse, parse_qs
88 |
89 | # First extract the base endpoint and deployment from path
90 | endpoint_pattern = r'(https://[^/]+\.openai\.azure\.com)(?:/openai/deployments/([^/\?]+))?'
91 | match = re.match(endpoint_pattern, base_url)
92 |
93 | if match:
94 | azure_endpoint = match.group(1)
95 | deployment_name = match.group(2) if match.group(2) else "unknown"
96 |
97 | # Extract API version from query parameters
98 | parsed_url = urlparse(base_url)
99 | query_params = parse_qs(parsed_url.query)
100 | api_version = query_params.get('api-version', ['2024-02-01'])[0]
101 |
102 | self.console.print(f"[dim] Extracted endpoint: {azure_endpoint}[/dim]")
103 | self.console.print(f"[dim] Extracted deployment: {deployment_name}[/dim]")
104 | self.console.print(f"[dim] Extracted API version: {api_version}[/dim]")
105 |
106 | return azure_endpoint, deployment_name, api_version
107 | else:
108 | self.console.print(f"[yellow]⚠️ Could not parse Azure URL: {base_url}[/yellow]")
109 | # Fallback: assume the base_url is the endpoint
110 | return base_url, "unknown", "2024-02-01"
111 |
112 | def _initialize_llm_client(self) -> Union[OpenAI, AzureOpenAI]:
113 | """
114 | Initialize LLM client (OpenAI or Azure OpenAI)
115 |
116 | Returns:
117 | Configured LLM client (OpenAI or AzureOpenAI)
118 | """
119 | if self.is_llm_azure:
120 | self.console.print("[cyan]🔧 Initializing Azure OpenAI LLM client[/cyan]")
121 | # For Azure OpenAI, extract azure_endpoint, deployment and api_version from base_url
122 | base_url = self.llm_config["base_url"]
123 | azure_endpoint, deployment_name, api_version = self._extract_azure_info(base_url)
124 |
125 | # Store deployment name for use in API calls
126 | self.llm_deployment = deployment_name
127 |
128 | self.console.print(f"[dim] Azure Endpoint: {azure_endpoint}[/dim]")
129 | self.console.print(f"[dim] Deployment: {deployment_name}[/dim]")
130 | self.console.print(f"[dim] API Version: {api_version}[/dim]")
131 |
132 | client = AzureOpenAI(
133 | azure_endpoint=azure_endpoint,
134 | api_key=self.llm_config["api_key"],
135 | api_version=api_version, # Use extracted API version
136 | )
137 | self.console.print("[green]✓ Azure OpenAI LLM client initialized[/green]")
138 | return client
139 | else:
140 | self.console.print("[cyan]🔧 Initializing standard OpenAI LLM client[/cyan]")
141 | self.console.print(f"[dim] Base URL: {self.llm_config['base_url']}[/dim]")
142 |
143 | client = OpenAI(
144 | base_url=self.llm_config["base_url"],
145 | api_key=self.llm_config["api_key"],
146 | )
147 | self.console.print("[green]✓ Standard OpenAI LLM client initialized[/green]")
148 | return client
149 |
150 | def _initialize_t2i_client(self) -> Union[OpenAI, AzureOpenAI]:
151 | """
152 | Initialize text-to-image client (OpenAI or Azure OpenAI)
153 |
154 | Returns:
155 | Configured text-to-image client (OpenAI or AzureOpenAI)
156 | """
157 | if self.is_t2i_azure:
158 | self.console.print("[cyan]🔧 Initializing Azure OpenAI T2I client[/cyan]")
159 | # For Azure OpenAI, extract azure_endpoint, deployment and api_version from base_url
160 | base_url = self.t2i_config["base_url"]
161 | azure_endpoint, deployment_name, api_version = self._extract_azure_info(base_url)
162 |
163 | # Store deployment name for use in API calls
164 | self.t2i_deployment = deployment_name
165 |
166 | self.console.print(f"[dim] Azure Endpoint: {azure_endpoint}[/dim]")
167 | self.console.print(f"[dim] Deployment: {deployment_name}[/dim]")
168 | self.console.print(f"[dim] API Version: {api_version}[/dim]")
169 |
170 | client = AzureOpenAI(
171 | azure_endpoint=azure_endpoint,
172 | api_key=self.t2i_config["api_key"],
173 | api_version=api_version, # Use extracted API version
174 | )
175 | self.console.print("[green]✓ Azure OpenAI T2I client initialized[/green]")
176 | return client
177 | else:
178 | self.console.print("[cyan]🔧 Initializing standard OpenAI T2I client[/cyan]")
179 | self.console.print(f"[dim] Base URL: {self.t2i_config['base_url']}[/dim]")
180 |
181 | client = OpenAI(
182 | base_url=self.t2i_config["base_url"],
183 | api_key=self.t2i_config["api_key"],
184 | )
185 | self.console.print("[green]✓ Standard OpenAI T2I client initialized[/green]")
186 | return client
187 |
188 | def get_answer(self, question: str, model: Optional[str] = None, max_retries: int = 3) -> str:
189 | """
190 | Get answer using LLM (with retry mechanism)
191 |
192 | Args:
193 | question: User question
194 | model: Specify model to use, if not specified use default model from config
195 | max_retries: Maximum retry attempts
196 |
197 | Returns:
198 | LLM answer
199 | """
200 | # For Azure OpenAI, use deployment name; for others, use model name
201 | if self.is_llm_azure and hasattr(self, 'llm_deployment'):
202 | model_name = self.llm_deployment
203 | specified_model = model or self.llm_config["model_name"]
204 | self.console.print(f"[cyan]🤖 Making Azure OpenAI LLM request[/cyan]")
205 | self.console.print(f"[dim] Using deployment: {model_name}[/dim]")
206 | self.console.print(f"[dim] Requested model: {specified_model}[/dim]")
207 | else:
208 | model_name = model or self.llm_config["model_name"]
209 | self.console.print(f"[cyan]🤖 Making LLM request[/cyan]")
210 | self.console.print(f"[dim] Model: {model_name}[/dim]")
211 |
212 | provider = 'Azure OpenAI' if self.is_llm_azure else 'OpenAI'
213 | self.console.print(f"[dim] Provider: {provider}[/dim]")
214 | self.console.print(f"[dim] Max retries: {max_retries}[/dim]")
215 |
216 | for attempt in range(max_retries):
217 | try:
218 | response = self.llm_client.chat.completions.create(
219 | model=model_name,
220 | messages=[
221 | {"role": "user", "content": question}
222 | ],
223 | max_tokens=self.max_tokens,
224 | temperature=self.temperature,
225 | timeout=60
226 | )
227 |
228 | answer = response.choices[0].message.content
229 | return answer
230 |
231 | except Exception as e:
232 | error_msg = str(e)
233 | self.console.print(f"[red]LLM request error (attempt {attempt + 1}/{max_retries}): {error_msg}[/red]")
234 |
235 | # Provide detailed error information
236 | self.console.print(f"[yellow]Model used: {model_name}[/yellow]")
237 | self.console.print(f"[yellow]Base URL: {self.llm_config.get('base_url', 'Unknown')}[/yellow]")
238 |
239 | # If this is the last attempt, raise exception
240 | if attempt == max_retries - 1:
241 | self.console.print(f"[red]All retry attempts failed, giving up request[/red]")
242 | raise Exception(f"LLM request failed after {max_retries} retries: {error_msg}")
243 | else:
244 | # Exponential backoff delay
245 | delay = 2 ** attempt
246 | self.console.print(f"[yellow]Waiting {delay} seconds before retry...[/yellow]")
247 | time.sleep(delay)
248 |
249 | def generate_text(self, prompt: str, model: Optional[str] = None) -> str:
250 | """
251 | Generate text using LLM (alias for get_answer)
252 |
253 | Args:
254 | prompt: Text prompt
255 | model: Specify model to use, if not specified use default model from config
256 |
257 | Returns:
258 | Generated text
259 | """
260 | return self.get_answer(prompt, model)
261 |
262 | def get_image(self, prompt: str, model: Optional[str] = None) -> Dict[str, Union[str, bytes, None]]:
263 | """
264 | Generate image using text-to-image model
265 |
266 | Args:
267 | prompt: Image description prompt
268 | model: Specify model to use, if not specified use default model from config
269 |
270 | Returns:
271 | Dictionary containing url and content: {"url": str, "content": bytes}
272 | """
273 | try:
274 | # For Azure OpenAI, use deployment name; for others, use model name
275 | if self.is_t2i_azure and hasattr(self, 't2i_deployment'):
276 | model_name = self.t2i_deployment
277 | specified_model = model or self.t2i_config["model_name"]
278 | self.console.print(f"[cyan]🎨 Making Azure OpenAI T2I request[/cyan]")
279 | self.console.print(f"[dim] Using deployment: {model_name}[/dim]")
280 | self.console.print(f"[dim] Requested model: {specified_model}[/dim]")
281 | else:
282 | model_name = model or self.t2i_config["model_name"]
283 | self.console.print(f"[cyan]🎨 Making T2I request[/cyan]")
284 | self.console.print(f"[dim] Model: {model_name}[/dim]")
285 |
286 | provider = 'Azure OpenAI' if self.is_t2i_azure else 'OpenAI'
287 | self.console.print(f"[dim] Provider: {provider}[/dim]")
288 | self.console.print(f"[dim] Image size: {self.image_size}[/dim]")
289 | self.console.print(f"[dim] Quality: {self.quality}[/dim]")
290 |
291 | # Generate image request parameters - start with basic params
292 | generate_params = {
293 | "model": model_name,
294 | "prompt": prompt,
295 | "n": 1
296 | }
297 |
298 | # Add size and quality parameters based on provider type
299 | if self.is_t2i_azure:
300 | self.console.print("[cyan]🔧 Configuring for Azure OpenAI[/cyan]")
301 | # For Azure OpenAI, use basic parameters
302 | generate_params["size"] = self.image_size
303 | # Azure OpenAI may support quality parameter for DALL-E models
304 | deployment_model = specified_model if self.is_t2i_azure and hasattr(self, 't2i_deployment') else model_name
305 | if deployment_model.startswith("dall-e"):
306 | generate_params["quality"] = self.quality
307 | self.console.print("[dim] Added quality parameter for DALL-E[/dim]")
308 |
309 | else:
310 | self.console.print("[cyan]🔧 Configuring for standard OpenAI[/cyan]")
311 | # For OpenAI and other OpenAI-compatible APIs
312 | base_url = self.t2i_config.get("base_url", "")
313 |
314 | if "openai.com" in base_url or model_name.startswith("dall-e"):
315 | generate_params["size"] = self.image_size
316 | # Add quality parameter only for dall-e models
317 | if model_name.startswith("dall-e"):
318 | generate_params["quality"] = self.quality
319 | self.console.print("[dim] Added quality parameter for DALL-E[/dim]")
320 | else:
321 | # For other providers (like Doubao/ByteDance), use basic parameters
322 | generate_params["size"] = self.image_size
323 | self.console.print("[dim] Using basic parameters for other provider[/dim]")
324 |
325 | # Don't add quality parameter for non-OpenAI providers
326 | # as it may cause "InvalidParameter" errors
327 |
328 | self.console.print(f"[cyan]📤 Sending request with parameters:[/cyan]")
329 | for key, value in generate_params.items():
330 | self.console.print(f"[dim] {key}: {value}[/dim]")
331 |
332 | response = self.t2i_client.images.generate(**generate_params)
333 |
334 | self.console.print("[green]✓ Image generation request successful[/green]")
335 |
336 | image_url = response.data[0].url
337 | self.console.print(f"[green]✓ Image URL received: {image_url}[/green]")
338 |
339 | # Download image content with retry mechanism
340 | self.console.print("[cyan]⬇️ Downloading image content...[/cyan]")
341 | image_content = self._download_image_with_retry(image_url, max_retries=3)
342 |
343 | if image_content:
344 | size_mb = len(image_content) / (1024 * 1024)
345 | self.console.print(f"[green]✓ Download successful: {len(image_content)} bytes ({size_mb:.2f} MB)[/green]")
346 | else:
347 | self.console.print("[yellow]⚠️ Image download failed, but URL is available[/yellow]")
348 |
349 | return {
350 | "url": image_url,
351 | "content": image_content
352 | }
353 |
354 | except Exception as e:
355 | self.console.print(f"[red]❌ Image generation failed: {e}[/red]")
356 | # Provide helpful error information
357 | self.console.print(f"[yellow]🔍 Debug information:[/yellow]")
358 | self.console.print(f"[dim] Model used: {model_name}[/dim]")
359 | self.console.print(f"[dim] Base URL: {self.t2i_config.get('base_url', 'Unknown')}[/dim]")
360 | self.console.print(f"[dim] Is Azure OpenAI: {self.is_t2i_azure}[/dim]")
361 | self.console.print(f"[dim] Error type: {type(e).__name__}[/dim]")
362 | raise
363 |
364 | def _download_image_with_retry(self, image_url: str, max_retries: int = 3) -> Optional[bytes]:
365 | """
366 | Download image with retry mechanism
367 |
368 | Args:
369 | image_url: Image URL
370 | max_retries: Maximum retry attempts
371 |
372 | Returns:
373 | Image content bytes, returns None if failed
374 | """
375 | import time
376 | import ssl
377 |
378 | for attempt in range(max_retries):
379 | try:
380 | self.console.print(f"[dim]📥 Download attempt {attempt + 1}/{max_retries}[/dim]")
381 |
382 | # Set request parameters with SSL tolerance
383 | session = requests.Session()
384 | session.verify = True # Verify SSL certificate
385 |
386 | # Add User-Agent and other headers
387 | headers = {
388 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
389 | 'Accept': 'image/*,*/*;q=0.8',
390 | 'Accept-Encoding': 'gzip, deflate, br',
391 | 'Connection': 'keep-alive'
392 | }
393 |
394 | response = session.get(
395 | image_url,
396 | timeout=60, # Increase timeout
397 | headers=headers,
398 | stream=True # Stream download
399 | )
400 | response.raise_for_status()
401 |
402 | # Get image content
403 | image_content = response.content
404 | self.console.print(f"Image downloaded successfully, size: {len(image_content)} bytes")
405 | return image_content
406 |
407 | except (requests.exceptions.SSLError, ssl.SSLError) as ssl_error:
408 | self.console.print(f"SSL error (attempt {attempt + 1}/{max_retries}): {str(ssl_error)}")
409 | if attempt == max_retries - 1:
410 | self.console.print("SSL connection failed consistently, possibly server certificate issue")
411 | else:
412 | time.sleep(2 ** attempt) # Exponential backoff
413 |
414 | except requests.exceptions.ConnectionError as conn_error:
415 | self.console.print(f"Connection error (attempt {attempt + 1}/{max_retries}): {str(conn_error)}")
416 | if attempt == max_retries - 1:
417 | self.console.print("Network connection failed consistently")
418 | else:
419 | time.sleep(2 ** attempt)
420 |
421 | except requests.exceptions.Timeout as timeout_error:
422 | self.console.print(f"Timeout error (attempt {attempt + 1}/{max_retries}): {str(timeout_error)}")
423 | if attempt == max_retries - 1:
424 | self.console.print("Request timeout")
425 | else:
426 | time.sleep(1)
427 |
428 | except Exception as e:
429 | self.console.print(f"Failed to download image (attempt {attempt + 1}/{max_retries}): {str(e)}")
430 | if attempt == max_retries - 1:
431 | self.console.print("All retry attempts failed")
432 | else:
433 | time.sleep(1)
434 |
435 | return None
436 |
437 | def get_current_settings(self) -> dict:
438 | """
439 | Get current settings information
440 |
441 | Returns:
442 | Current settings dictionary
443 | """
444 | return {
445 | "llm_base_url": self.llm_config["base_url"],
446 | "llm_model_name": self.llm_config["model_name"],
447 | "llm_is_azure": self.is_llm_azure,
448 | "t2i_base_url": self.t2i_config["base_url"],
449 | "t2i_model_name": self.t2i_config["model_name"],
450 | "t2i_is_azure": self.is_t2i_azure,
451 | "max_tokens": self.max_tokens,
452 | "temperature": self.temperature,
453 | "image_size": self.image_size,
454 | "quality": self.quality
455 | }
456 |
457 |
458 | def main():
459 | """Main function demonstrating model client usage"""
460 | console = Console()
461 | try:
462 | # Create model client instance
463 | client = ModelClient()
464 |
465 | # Display current configuration information
466 | console.print("=== Current Configuration ===")
467 | settings = client.get_current_settings()
468 | for key, value in settings.items():
469 | console.print(f"{key}: {value}")
470 | console.print()
471 |
472 | # Test LLM Q&A functionality
473 | console.print("=== LLM Q&A Test ===")
474 | question = "What is artificial intelligence? Please answer briefly in 50 words or less."
475 | answer = client.get_answer(question)
476 | console.print(f"Question: {question}")
477 | console.print(f"Answer: {answer}")
478 | console.print()
479 |
480 | # Test text-to-image functionality
481 | console.print("=== Text-to-Image Test ===")
482 | image_prompt = "A cute cat playing in a garden, cartoon style"
483 | image_result = client.get_image(image_prompt)
484 | console.print(f"Image description: {image_prompt}")
485 |
486 | if "error" in image_result:
487 | console.print(f"Generation failed: {image_result['error']}")
488 | else:
489 | console.print(f"Generated image URL: {image_result['url']}")
490 | if image_result['content']:
491 | content_size = len(image_result['content'])
492 | console.print(f"Image content size: {content_size} bytes")
493 | # Option to save image locally
494 | # with open("generated_image.png", "wb") as f:
495 | # f.write(image_result['content'])
496 | # console.print("Image saved as generated_image.png")
497 | else:
498 | console.print("Image content download failed")
499 | console.print()
500 |
501 | console.print("=== Program completed ===")
502 |
503 | except Exception as e:
504 | console.print(f"Program error: {str(e)}")
505 |
506 |
507 | if __name__ == "__main__":
508 | main()
--------------------------------------------------------------------------------