Skip to content

Commit 7c5a904

Browse files
authored
Merge pull request #12 from divar-ir/fix-partial-analyze
[FEAT] Fix intermittent cronjob failures with partial analysis tolerance and configurable retries
2 parents 15b439b + 8499a6d commit 7c5a904

File tree

9 files changed

+208
-47
lines changed

9 files changed

+208
-47
lines changed

.env.sample

Lines changed: 131 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,148 @@
1+
# ============================================================================
2+
# AI Doc Gen - Environment Configuration
3+
# ============================================================================
4+
#
5+
# This file contains all configurable environment variables for the AI Doc Gen
6+
# application. Copy this file to `.env` and update the values as needed.
7+
#
8+
# 📋 Quick Setup Checklist:
9+
# 1. Copy this file: cp .env.sample .env
10+
# 2. Set your LLM API keys and base URLs
11+
# 3. Configure GitLab integration (for cronjob mode)
12+
# 4. Adjust retry/timeout settings based on your environment
13+
# 5. Enable observability tools if needed (Langfuse)
14+
#
15+
# 🚀 For rate-limited environments, increase retry values:
16+
# - ANALYZER_AGENT_RETRIES=5
17+
# - TOOL_FILE_READER_MAX_RETRIES=5
18+
# - HTTP_RETRY_MAX_ATTEMPTS=10
19+
# ============================================================================
20+
21+
# ------------- Core Application Settings ----------
122
PYTHONPATH=src
2-
ENVIRONMENT=development
23+
ENVIRONMENT=development # Options: development, staging, production
324

4-
# ------------ Langfuse ------------
5-
ENABLE_LANGFUSE=false
6-
OTEL_SDK_DISABLED=false
25+
# ------------- Observability & Telemetry ----------
26+
# Langfuse provides LLM observability and analytics
27+
ENABLE_LANGFUSE=false # Set to 'true' to enable Langfuse tracking
28+
OTEL_SDK_DISABLED=false # OpenTelemetry SDK control
729

30+
# Langfuse Configuration (only needed if ENABLE_LANGFUSE=true)
831
LANGFUSE_PUBLIC_KEY=YOUR_PUBLIC_KEY_HERE
932
LANGFUSE_SECRET_KEY=YOUR_SECRET_KEY_HERE
1033
LANGFUSE_HOST=https://YOUR_LANGFUSE_HOST_HERE
1134
OTEL_EXPORTER_OTLP_ENDPOINT=YOUR_OTEL_ENDPOINT_HERE
1235

13-
# ------------ LLM Agents ------------
36+
# ============================================================================
37+
# 🤖 LLM AGENTS CONFIGURATION
38+
# ============================================================================
39+
40+
# ------------- Analyzer Agent (Code Analysis) ----------
41+
# The analyzer agent performs code structure, dependency, data flow, and API analysis
1442
ANALYZER_LLM_MODEL=claude-sonnet-4-20250514
15-
ANALYZER_LLM_BASE_URL=YOUR_ANALYZER_BASE_URL_HERE
16-
ANALYZER_LLM_API_KEY=YOUR_ANALYZER_API_KEY_HERE
17-
ANALYZER_PARALLEL_TOOL_CALLS=true
43+
ANALYZER_LLM_BASE_URL=YOUR_ANALYZER_BASE_URL_HERE # e.g., https://api.anthropic.com
44+
ANALYZER_LLM_API_KEY=YOUR_ANALYZER_API_KEY_HERE # Your LLM provider API key
45+
ANALYZER_PARALLEL_TOOL_CALLS=true # Enable parallel tool execution
46+
47+
# Analyzer Agent Behavior Settings
48+
ANALYZER_AGENT_RETRIES=2 # Retries per analysis agent on failure
49+
ANALYZER_LLM_TIMEOUT=180 # Request timeout in seconds (3 minutes)
50+
ANALYZER_LLM_MAX_TOKENS=8192 # Maximum tokens in LLM responses
51+
ANALYZER_LLM_TEMPERATURE=0.0 # Response randomness (0.0=deterministic, 1.0=creative)
1852

53+
# ------------- Documenter Agent (README Generation) ----------
54+
# The documenter agent generates comprehensive README.md files
1955
DOCUMENTER_LLM_MODEL=claude-sonnet-4-20250514
2056
DOCUMENTER_LLM_BASE_URL=YOUR_DOCUMENTER_BASE_URL_HERE
2157
DOCUMENTER_LLM_API_KEY=YOUR_DOCUMENTER_API_KEY_HERE
2258
DOCUMENTER_PARALLEL_TOOL_CALLS=true
2359

24-
# ------------- Gitlab ----------
25-
GITLAB_API_URL=https://git.divar.cloud
26-
GITLAB_USER_NAME=AI Analyzer
27-
GITLAB_USER_USERNAME=agent_doc
28-
GITLAB_USER_EMAIL=YOUR_EMAIL_HERE
29-
GITLAB_OAUTH_TOKEN=YOUR_GITLAB_TOKEN_HERE
60+
# Documenter Agent Behavior Settings
61+
DOCUMENTER_AGENT_RETRIES=2 # Retries for documenter agent on failure
62+
DOCUMENTER_LLM_TIMEOUT=180 # Request timeout in seconds
63+
DOCUMENTER_LLM_MAX_TOKENS=8192 # Maximum tokens in LLM responses
64+
DOCUMENTER_LLM_TEMPERATURE=0.0 # Response randomness (0.0=deterministic)
65+
66+
# ============================================================================
67+
# 🔧 RETRY & RESILIENCE CONFIGURATION
68+
# ============================================================================
69+
70+
# ------------- Agent Tools Settings ----------
71+
# These settings control the retry behavior for internal tools used by agents
72+
TOOL_FILE_READER_MAX_RETRIES=2 # File reading tool retry attempts
73+
TOOL_LIST_FILES_MAX_RETRIES=2 # File listing tool retry attempts
74+
75+
# ------------- HTTP Retry Client ----------
76+
# Controls retry behavior for all HTTP requests to LLM providers
77+
# 💡 Increase these values if you encounter rate limiting issues
78+
HTTP_RETRY_MAX_ATTEMPTS=5 # Total HTTP retry attempts before failure
79+
HTTP_RETRY_MULTIPLIER=1 # Exponential backoff multiplier (1s→2s→4s→8s)
80+
HTTP_RETRY_MAX_WAIT_PER_ATTEMPT=60 # Maximum wait between attempts (seconds)
81+
HTTP_RETRY_MAX_TOTAL_WAIT=300 # Maximum total wait time (5 minutes)
82+
83+
# ⚡ Rate Limiting Solutions:
84+
# If you encounter "max retries exceeded" or "request limit" errors:
85+
# - Increase HTTP_RETRY_MAX_ATTEMPTS to 10+
86+
# - Increase ANALYZER_AGENT_RETRIES to 5+
87+
# - Increase timeout values (ANALYZER_LLM_TIMEOUT=300+)
88+
89+
# ============================================================================
90+
# 🔗 GITLAB INTEGRATION (Required for Cronjob Mode)
91+
# ============================================================================
92+
93+
# GitLab API Configuration
94+
GITLAB_API_URL=https://git.divar.cloud # Your GitLab instance URL
95+
GITLAB_USER_NAME=AI Analyzer # Display name for commits/MRs
96+
GITLAB_USER_USERNAME=agent_doc # GitLab username for the bot
97+
GITLAB_USER_EMAIL=YOUR_EMAIL_HERE # Email for Git commits
98+
GITLAB_OAUTH_TOKEN=YOUR_GITLAB_TOKEN_HERE # GitLab access token with repo permissions
99+
100+
# 🔑 GitLab Token Permissions Required:
101+
# - api (full API access)
102+
# - read_repository (read repo contents)
103+
# - write_repository (create branches, commits)
104+
105+
# ============================================================================
106+
# 📊 LOGGING & DEBUGGING
107+
# ============================================================================
108+
109+
# Logging Configuration
110+
CONSOLE_LOG_LEVEL=WARNING # Console output level
111+
FILE_LOG_LEVEL=INFO # File logging level
112+
113+
# Available log levels (from most to least verbose):
114+
# DEBUG - Detailed debugging information
115+
# INFO - General information messages
116+
# WARNING - Warning messages (default for console)
117+
# ERROR - Error messages only
118+
# CRITICAL - Critical errors only
119+
120+
# 🐛 For debugging issues:
121+
# Set CONSOLE_LOG_LEVEL=DEBUG to see detailed operation logs
122+
123+
# ============================================================================
124+
# 🚀 DEPLOYMENT-SPECIFIC RECOMMENDATIONS
125+
# ============================================================================
126+
127+
# Development Environment:
128+
# - Use DEBUG log levels for troubleshooting
129+
# - Lower retry counts for faster feedback
130+
# - Enable Langfuse for observability
131+
132+
# Production Environment:
133+
# - Use WARNING/ERROR log levels
134+
# - Higher retry counts for resilience
135+
# - Monitor with Langfuse/OTEL
136+
# - Set appropriate timeout values based on your LLM provider
30137

31-
# ------------- App ----------
32-
APP_VERSION=1.1.0
138+
# Rate-Limited Environments:
139+
# - ANALYZER_AGENT_RETRIES=5+
140+
# - HTTP_RETRY_MAX_ATTEMPTS=10+
141+
# - HTTP_RETRY_MAX_TOTAL_WAIT=600+
142+
# - ANALYZER_LLM_TIMEOUT=300+
33143

34-
CONSOLE_LOG_LEVEL=WARNING # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
35-
FILE_LOG_LEVEL=INFO # Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
144+
# High-Performance Environments:
145+
# - ANALYZER_PARALLEL_TOOL_CALLS=true
146+
# - DOCUMENTER_PARALLEL_TOOL_CALLS=true
147+
# - Higher ANALYZER_LLM_MAX_TOKENS values
148+
# - Lower timeout values for faster failures

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ai-doc-gen"
3-
version = "1.0.0"
3+
version = "1.2.0"
44
description = "AI-powered code documentation generator that analyzes repositories and creates comprehensive documentation"
55
authors = [{ name = "Milad Noroozi", email = "norooziosos@gmail.com" }]
66
requires-python = ">=3.13,<3.14"

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0"
1+
__version__ = "1.2.0"

src/agents/analyzer.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,14 @@ async def run(self):
123123
# Log results for each agent
124124
for i, result in enumerate(results):
125125
if isinstance(result, Exception):
126-
Logger.error(f"Agent {i} failed: {result}")
126+
Logger.error(
127+
f"Agent {tasks[i].agent.name} failed: {result}",
128+
exc_info=True,
129+
)
127130
else:
128-
Logger.info(f"Agent {i} completed successfully")
131+
Logger.info(
132+
f"Agent {tasks[i].agent.name} completed successfully",
133+
)
129134

130135
self.validate_succession(analysis_files)
131136

@@ -135,10 +140,23 @@ def validate_succession(self, analysis_files: List[Path]):
135140
if not file.exists():
136141
missing_files.append(file)
137142

138-
if missing_files:
139-
missing_files_str = ", ".join([str(file) for file in missing_files])
140-
Logger.warning(f"Some analysis files not found: {missing_files_str}")
141-
raise ValueError(f"Some analysis files not found: {missing_files_str}")
143+
if not missing_files:
144+
# All files exist - complete success
145+
Logger.info(f"All {len(analysis_files)} analysis files generated successfully")
146+
return
147+
148+
if len(missing_files) == len(analysis_files):
149+
# ALL files missing - complete failure
150+
Logger.error("Complete analysis failure: no analysis files were generated")
151+
raise ValueError("Complete analysis failure: no analysis files were generated")
152+
153+
# SOME files missing - partial success, log warning but continue
154+
missing_files_str = ", ".join([str(file) for file in missing_files])
155+
successful_count = len(analysis_files) - len(missing_files)
156+
Logger.warning(
157+
f"Partial analysis success: {successful_count}/{len(analysis_files)} files generated. Missing: {missing_files_str}"
158+
)
159+
# Continue without raising error - partial results are better than no results
142160

143161
async def _run_agent(self, agent: Agent, user_prompt: str, file_path: Path):
144162
trace.get_current_span().add_event(name=f"Running {agent.name}", attributes={"agent_name": agent.name})
@@ -194,9 +212,9 @@ def _llm_model(self) -> Tuple[Model, ModelSettings]:
194212
)
195213

196214
settings = ModelSettings(
197-
temperature=0.0,
198-
max_tokens=8192,
199-
timeout=180,
215+
temperature=config.ANALYZER_LLM_TEMPERATURE,
216+
max_tokens=config.ANALYZER_LLM_MAX_TOKENS,
217+
timeout=config.ANALYZER_LLM_TIMEOUT,
200218
parallel_tool_calls=config.ANALYZER_PARALLEL_TOOL_CALLS,
201219
)
202220

@@ -211,7 +229,7 @@ def _structure_analyzer_agent(self) -> Agent:
211229
model=model,
212230
model_settings=model_settings,
213231
output_type=AnalyzerResult,
214-
retries=2,
232+
retries=config.ANALYZER_AGENT_RETRIES,
215233
system_prompt=self._render_prompt("agents.structure_analyzer.system_prompt"),
216234
tools=[
217235
FileReadTool().get_tool(),
@@ -229,7 +247,7 @@ def _data_flow_analyzer_agent(self) -> Agent:
229247
model=model,
230248
model_settings=model_settings,
231249
output_type=str,
232-
retries=2,
250+
retries=config.ANALYZER_AGENT_RETRIES,
233251
system_prompt=self._render_prompt("agents.data_flow_analyzer.system_prompt"),
234252
tools=[
235253
FileReadTool().get_tool(),
@@ -247,7 +265,7 @@ def _dependency_analyzer_agent(self) -> Agent:
247265
model=model,
248266
model_settings=model_settings,
249267
output_type=str,
250-
retries=2,
268+
retries=config.ANALYZER_AGENT_RETRIES,
251269
system_prompt=self._render_prompt("agents.dependency_analyzer.system_prompt"),
252270
tools=[
253271
FileReadTool().get_tool(),
@@ -265,7 +283,7 @@ def _request_flow_analyzer_agent(self) -> Agent:
265283
model=model,
266284
model_settings=model_settings,
267285
output_type=str,
268-
retries=2,
286+
retries=config.ANALYZER_AGENT_RETRIES,
269287
system_prompt=self._render_prompt("agents.request_flow_analyzer.system_prompt"),
270288
tools=[
271289
FileReadTool().get_tool(),
@@ -283,7 +301,7 @@ def _api_analyzer_agent(self) -> Agent:
283301
model=model,
284302
model_settings=model_settings,
285303
output_type=AnalyzerResult,
286-
retries=2,
304+
retries=config.ANALYZER_AGENT_RETRIES,
287305
system_prompt=self._render_prompt("agents.api_analyzer.system_prompt"),
288306
tools=[
289307
FileReadTool().get_tool(),

src/agents/documenter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ def _llm_model(self) -> Tuple[Model, ModelSettings]:
151151
)
152152

153153
settings = ModelSettings(
154-
temperature=0.0,
155-
max_tokens=8192,
156-
timeout=180,
154+
temperature=config.DOCUMENTER_LLM_TEMPERATURE,
155+
max_tokens=config.DOCUMENTER_LLM_MAX_TOKENS,
156+
timeout=config.DOCUMENTER_LLM_TIMEOUT,
157157
parallel_tool_calls=config.DOCUMENTER_PARALLEL_TOOL_CALLS,
158158
)
159159

@@ -168,7 +168,7 @@ def _documenter_agent(self) -> Agent:
168168
model=model,
169169
model_settings=model_settings,
170170
output_type=DocumenterResult,
171-
retries=2,
171+
retries=config.DOCUMENTER_AGENT_RETRIES,
172172
system_prompt=self._render_prompt("agents.documenter.system_prompt"),
173173
tools=[
174174
FileReadTool().get_tool(),

src/agents/tools/dir_tool/list_files.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from opentelemetry import trace
77
from pydantic_ai import Tool
88

9+
import config
910
from utils import Logger
1011

1112

@@ -229,7 +230,7 @@ def __init__(
229230
self.ignored_extensions = ignored_extensions or []
230231

231232
def get_tool(self):
232-
return Tool(self._run, name="List-Files-Tool", takes_ctx=False, max_retries=2)
233+
return Tool(self._run, name="List-Files-Tool", takes_ctx=False, max_retries=config.TOOL_LIST_FILES_MAX_RETRIES)
233234

234235
def _run(self, directory: str) -> Any:
235236
"""List files in a directory recursively, grouping them by directory.

src/agents/tools/file_tool/file_reader.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from opentelemetry import trace
44
from pydantic_ai import ModelRetry, Tool
55

6+
import config
67
from utils import Logger
78

89

@@ -11,7 +12,7 @@ def __init__(self):
1112
pass
1213

1314
def get_tool(self):
14-
return Tool(self._run, name="Read-File", takes_ctx=False, max_retries=2)
15+
return Tool(self._run, name="Read-File", takes_ctx=False, max_retries=config.TOOL_FILE_READER_MAX_RETRIES)
1516

1617
def _run(self, file_path: str, line_number: int = 0, line_count: int = 200) -> str:
1718
"""Read a file and return its contents.

src/config.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,32 @@ def str_to_bool(value: str) -> bool:
2020
else:
2121
raise ValueError(f"Invalid boolean value: {value}")
2222

23+
VERSION = os.getenv("APP_VERSION", "1.2.0")
2324

2425
# Analyzer
2526
ANALYZER_LLM_MODEL = os.environ["ANALYZER_LLM_MODEL"]
2627
ANALYZER_LLM_BASE_URL = os.environ["ANALYZER_LLM_BASE_URL"]
2728
ANALYZER_LLM_API_KEY = os.environ["ANALYZER_LLM_API_KEY"]
2829
ANALYZER_PARALLEL_TOOL_CALLS = str_to_bool(os.getenv("ANALYZER_PARALLEL_TOOL_CALLS", "true"))
2930

31+
# Analyzer Agent Settings
32+
ANALYZER_AGENT_RETRIES = int(os.getenv("ANALYZER_AGENT_RETRIES", "2"))
33+
ANALYZER_LLM_TIMEOUT = int(os.getenv("ANALYZER_LLM_TIMEOUT", "180"))
34+
ANALYZER_LLM_MAX_TOKENS = int(os.getenv("ANALYZER_LLM_MAX_TOKENS", "8192"))
35+
ANALYZER_LLM_TEMPERATURE = float(os.getenv("ANALYZER_LLM_TEMPERATURE", "0.0"))
36+
3037
# Documenter
3138
DOCUMENTER_LLM_MODEL = os.environ["DOCUMENTER_LLM_MODEL"]
3239
DOCUMENTER_LLM_BASE_URL = os.environ["DOCUMENTER_LLM_BASE_URL"]
3340
DOCUMENTER_LLM_API_KEY = os.environ["DOCUMENTER_LLM_API_KEY"]
3441
DOCUMENTER_PARALLEL_TOOL_CALLS = str_to_bool(os.getenv("DOCUMENTER_PARALLEL_TOOL_CALLS", "true"))
3542

43+
# Documenter Agent Settings
44+
DOCUMENTER_AGENT_RETRIES = int(os.getenv("DOCUMENTER_AGENT_RETRIES", "2"))
45+
DOCUMENTER_LLM_TIMEOUT = int(os.getenv("DOCUMENTER_LLM_TIMEOUT", "180"))
46+
DOCUMENTER_LLM_MAX_TOKENS = int(os.getenv("DOCUMENTER_LLM_MAX_TOKENS", "8192"))
47+
DOCUMENTER_LLM_TEMPERATURE = float(os.getenv("DOCUMENTER_LLM_TEMPERATURE", "0.0"))
48+
3649
# Langfuse
3750
ENABLE_LANGFUSE = str_to_bool(os.getenv("ENABLE_LANGFUSE", "false"))
3851
LANGFUSE_PUBLIC_KEY = os.getenv("LANGFUSE_PUBLIC_KEY")
@@ -48,11 +61,20 @@ def str_to_bool(value: str) -> bool:
4861
# General
4962
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
5063

51-
VERSION = os.getenv("APP_VERSION", "1.0.0")
5264

5365
CONSOLE_LOG_LEVEL = getattr(logging, os.getenv("CONSOLE_LOG_LEVEL", "DEBUG").upper())
5466
FILE_LOG_LEVEL = getattr(logging, os.getenv("FILE_LOG_LEVEL", "INFO").upper())
5567

68+
# Agent Tools Settings
69+
TOOL_FILE_READER_MAX_RETRIES = int(os.getenv("TOOL_FILE_READER_MAX_RETRIES", "2"))
70+
TOOL_LIST_FILES_MAX_RETRIES = int(os.getenv("TOOL_LIST_FILES_MAX_RETRIES", "2"))
71+
72+
# HTTP Retry Client Settings
73+
HTTP_RETRY_MAX_ATTEMPTS = int(os.getenv("HTTP_RETRY_MAX_ATTEMPTS", "5"))
74+
HTTP_RETRY_MULTIPLIER = int(os.getenv("HTTP_RETRY_MULTIPLIER", "1"))
75+
HTTP_RETRY_MAX_WAIT_PER_ATTEMPT = int(os.getenv("HTTP_RETRY_MAX_WAIT_PER_ATTEMPT", "60"))
76+
HTTP_RETRY_MAX_TOTAL_WAIT = int(os.getenv("HTTP_RETRY_MAX_TOTAL_WAIT", "300"))
77+
5678
# --------------------------
5779
# Helper Function
5880

0 commit comments

Comments
 (0)