hari-huynh commited on
Commit
0447128
·
1 Parent(s): 1471dd3

Change Slack plain text to Markdown format

Browse files
Files changed (6) hide show
  1. DIFF.md +313 -0
  2. README.md +63 -0
  3. agent/code_review.py +157 -43
  4. requirements.txt +2 -1
  5. test_logfire.py +78 -0
  6. tool.py +1 -1
DIFF.md ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/.env b/.env
2
+ new file mode 100644
3
+ index 0000000..a392d55
4
+ --- /dev/null
5
+ +++ b/.env
6
+ @@ -0,0 +1,3 @@
7
+ +WORKSPACE_ID=thuantran_smulie
8
+
9
+ diff --git a/.gitignore b/.gitignore
10
+ index b24d71e..1a2f934 100644
11
+ --- a/.gitignore
12
+ +++ b/.gitignore
13
+ @@ -48,3 +48,4 @@ Thumbs.db
14
+ *.mov
15
+ *.wmv
16
+
17
+ +.env
18
+
19
+ diff --git a/PR.md b/PR.md
20
+ new file mode 100644
21
+ index 0000000..8416126
22
+ --- /dev/null
23
+ +++ b/PR.md
24
+ @@ -0,0 +1,20 @@
25
+ +
26
+ +# [pr-code-review] PR #2: Upload change in README.md
27
+ +*Change documentation in README.md file*
28
+ +
29
+ +**🌿 Branch Information:**
30
+ +- **Source Branch:** feature/pull-request → **Target Branch:** main
31
+ +
32
+ +**👤 Người tạo:** Hai Huynh
33
+ +
34
+ +**📅 Thời gian tạo:** 2024-08-20 14:30:00 +07:00
35
+ +
36
+ +**👥 Reviewers:**
37
+ +None
38
+ +
39
+ +**🔗 Link Pull Request:** [PR #2: Implement user authentication system](https://bitbucket.org/thuantran_smulie/pr-code-review/pull-requests/2)
40
+ +
41
+ +---
42
+ +
43
+ +## 📝 Changelogs:
44
+ +
45
+
46
+ diff --git a/README.md b/README.md
47
+ index c0a0828..f32c700 100644
48
+ --- a/README.md
49
+ +++ b/README.md
50
+ @@ -1 +1,3 @@
51
+ -Trigger khi có PR được tạo/thay đổi và gửi tin nhắn về Slack
52
+
53
+ +Trigger khi có PR được tạo/thay đổi và gửi tin nhắn về Slack
54
+ +Review code và gửi thông tin về Slack
55
+ +Gửi báo cáo về Slack
56
+ diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
57
+ index 35d5eb0..7206a5e 100644
58
+ --- a/bitbucket-pipelines.yml
59
+ +++ b/bitbucket-pipelines.yml
60
+ @@ -4,6 +4,42 @@ pipelines:
61
+ - step:
62
+ name: Build & Test on PR
63
+ script:
64
+ - - echo "Running build for PR"
65
+ - - npm install
66
+ - - npm test
67
+ + - echo "=== PULL REQUEST INFORMATION ==="
68
+ + - echo $BITBUCKET_PR_ID
69
+ + - echo "=== BRANCH INFORMATION ==="
70
+ + - echo $BITBUCKET_BRANCH
71
+ + - echo $BITBUCKET_PR_DESTINATION_BRANCH
72
+ + - echo "=== COMMIT INFORMATION ==="
73
+ + - echo $BITBUCKET_COMMIT
74
+ + - echo "=== REPOSITORY INFORMATION ==="
75
+ + - echo $BITBUCKET_REPO_SLUG
76
+ + - echo $BITBUCKET_REPO_UUID
77
+ + - echo $BITBUCKET_WORKSPACE
78
+ + - echo "=== PIPELINE INFORMATION ==="
79
+ + - echo $BITBUCKET_PIPELINE_UUID
80
+ + - echo $BITBUCKET_BUILD_NUMBER
81
+ + - echo $BITBUCKET_STEP_UUID
82
+ + - echo $BITBUCKET_STEP_NAME
83
+ + - echo $BITBUCKET_STEP_RUN_NUMBER
84
+ + - echo "=== ENVIRONMENT VARIABLES ==="
85
+ + - echo "All PR-related environment variables:"
86
+ + - env | grep BITBUCKET_PR
87
+ + - echo "All Bitbucket environment variables:"
88
+ + - env | grep BITBUCKET
89
+ +
90
+ + - step:
91
+ + name: Run Python Script
92
+ + image: python:3.11
93
+ + script:
94
+ + - pip install -r requirements.txt
95
+ + - echo "=== RUNNING TOOL SCRIPT ==="
96
+ +
97
+ + - step:
98
+ + name: Notify Slack
99
+ + script:
100
+ + - export MESSAGE="$(cat PR.md)"
101
+ + - pipe: atlassian/slack-notify:2.3.1
102
+ + variables:
103
+ + WEBHOOK_URL: $SLACK_WEBHOOK_URL
104
+ + MESSAGE: $MESSAGE
105
+
106
+ diff --git a/requirements.txt b/requirements.txt
107
+ new file mode 100644
108
+ index 0000000..e6442c3
109
+ --- /dev/null
110
+ +++ b/requirements.txt
111
+ @@ -0,0 +1,2 @@
112
+ +dotenv
113
+ +requests
114
+
115
+ diff --git a/tool.py b/tool.py
116
+ new file mode 100644
117
+ index 0000000..8062704
118
+ --- /dev/null
119
+ +++ b/tool.py
120
+ @@ -0,0 +1,201 @@
121
+ +import os
122
+ +import json
123
+ +import argparse
124
+ +from typing import Dict, Any, List, Optional, Tuple
125
+ +from dotenv import load_dotenv
126
+ +
127
+ +import requests
128
+ +
129
+ +# Set up ENVIRONMENT VARIABLE
130
+ +load_dotenv()
131
+ +# WORKSPACE_ID = os.getenv("WORKSPACE_ID")
132
+ +# BITBUCKET_USERNAME = os.getenv("BITBUCKET_USERNAME")
133
+ +DEFAULT_BASE_URL = "https://api.bitbucket.org/2.0"
134
+ +
135
+ +
136
+ +def create_session(username: Optional[str] = None, app_password: Optional[str] = None) -> requests.Session:
137
+ + resolved_username = username or os.getenv("BITBUCKET_USERNAME")
138
+ +
139
+ + if not resolved_username or not resolved_app_password:
140
+ + raise ValueError(
141
+ + "Missing credentials. Provide --username and --app-password or set env vars "
142
+ + )
143
+ +
144
+ + session = requests.Session()
145
+ + session.auth = (resolved_username, resolved_app_password)
146
+ + session.headers.update({"Accept": "application/json"})
147
+ + return session
148
+ +
149
+ +
150
+ +def _request_json(session: requests.Session, method: str, url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
151
+ + response = session.request(method, url, params=params, timeout=30)
152
+ + response.raise_for_status()
153
+ + return response.json()
154
+ +
155
+ +
156
+ +def _paginate_all(session: requests.Session, url: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
157
+ + """
158
+ + Retrieve all pages for Bitbucket v2.0 endpoints that return 'values' and 'next'.
159
+ + """
160
+ + aggregated_values: List[Dict[str, Any]] = []
161
+ + next_url: Optional[str] = url
162
+ + query_params: Dict[str, Any] = dict(params or {})
163
+ + if "pagelen" not in query_params:
164
+ + query_params["pagelen"] = 100
165
+ +
166
+ + while next_url:
167
+ + data = _request_json(session, "GET", next_url, params=query_params if next_url == url else None)
168
+ + values = data.get("values", [])
169
+ + aggregated_values.extend(values)
170
+ + next_url = data.get("next")
171
+ + return aggregated_values
172
+ +
173
+ +
174
+ +def get_pull_request_overview(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> Dict[str, Any]:
175
+ + url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}"
176
+ + return _request_json(session, "GET", url)
177
+ +
178
+ +
179
+ +def get_pull_request_commits(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
180
+ + url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/commits"
181
+ + return _paginate_all(session, url)
182
+ +
183
+ +
184
+ +def get_pull_request_comments(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
185
+ + url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments"
186
+ + return _paginate_all(session, url)
187
+ +
188
+ +
189
+ +def get_pull_request_activity(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
190
+ + url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/activity"
191
+ + return _paginate_all(session, url)
192
+ +
193
+ +
194
+ +def get_pull_request_diff(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> str:
195
+ + """
196
+ + Returns unified diff text for the PR. This endpoint returns text/plain.
197
+ + """
198
+ + url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diff"
199
+ + headers = {"Accept": "text/plain"}
200
+ + response = session.get(url, headers=headers, timeout=60)
201
+ + response.raise_for_status()
202
+ + return response.text
203
+ +
204
+ +
205
+ +def get_pull_request_full_data(
206
+ + session: requests.Session,
207
+ + workspace: str,
208
+ + repo_slug: str,
209
+ + pr_id: int,
210
+ + include: Optional[List[str]] = None,
211
+ + base_url: str = DEFAULT_BASE_URL,
212
+ +) -> Dict[str, Any]:
213
+ + """
214
+ + Fetch a comprehensive view of a Bitbucket Cloud pull request:
215
+ + - overview: PR metadata
216
+ + - commits: all commits attached to the PR
217
+ + - comments: all comments (including inline)
218
+ + - activity: all activity events for the PR
219
+ + - diff: unified diff text
220
+ +
221
+ + 'include' can limit which sections to fetch. Valid values: overview, commits, comments, activity, diff.
222
+ + """
223
+ + sections = ["overview", "commits", "comments", "activity", "diff"]
224
+ +
225
+ + result: Dict[str, Any] = {}
226
+ +
227
+ + if "overview" in sections:
228
+ + result["overview"] = get_pull_request_overview(session, workspace, repo_slug, pr_id, base_url)
229
+ + if "commits" in sections:
230
+ + result["commits"] = get_pull_request_commits(session, workspace, repo_slug, pr_id, base_url)
231
+ + if "comments" in sections:
232
+ + result["comments"] = get_pull_request_comments(session, workspace, repo_slug, pr_id, base_url)
233
+ + if "activity" in sections:
234
+ + result["activity"] = get_pull_request_activity(session, workspace, repo_slug, pr_id, base_url)
235
+ + if "diff" in sections:
236
+ + result["diff"] = get_pull_request_diff(session, workspace, repo_slug, pr_id, base_url)
237
+ +
238
+ + return result
239
+ +
240
+ +
241
+ +def save_md_report(filename, content):
242
+ + with open(filename, "w", encoding="utf-8") as f:
243
+ + f.write(content)
244
+ + print(f"Saved {filename} file")
245
+ +
246
+ +def main() -> None:
247
+ + parser = argparse.ArgumentParser(description="Fetch full details of a Bitbucket Cloud pull request.")
248
+ + parser.add_argument("--username", required=True, help="Bitbucket username")
249
+ + parser.add_argument("--password", required=True, help="Bitbucket app password")
250
+ + parser.add_argument("--workspace", required=True, help="Bitbucket workspace ID or slug")
251
+ + parser.add_argument("--repo", required=True, help="Repository slug")
252
+ + parser.add_argument("--pr", required=True, type=int, help="Pull request ID")
253
+ + # parser.add_argument("--username", help="Bitbucket username (or set BITBUCKET_USERNAME)")
254
+ + # parser.add_argument(
255
+ + # "--include",
256
+ + # nargs="*",
257
+ + # choices=["overview", "commits", "comments", "activity", "diff"],
258
+ + # help="Sections to include. Default: all",
259
+ + # )
260
+ + # parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base API URL. Default is Bitbucket Cloud v2.0")
261
+ + args = parser.parse_args()
262
+ +
263
+ + session = create_session(args.username, args.password)
264
+ +
265
+ + data = get_pull_request_overview(
266
+ + session = session,
267
+ + workspace = args.workspace,
268
+ + repo_slug = args.repo,
269
+ + pr_id = args.pr,
270
+ + base_url = DEFAULT_BASE_URL
271
+ + )
272
+ +
273
+ + # Lấy các thông tin cần thiết
274
+ + title = data["title"]
275
+ + description = data["description"]
276
+ + source_branch = data["source"]["branch"]["name"]
277
+ + destination_branch = data["destination"]["branch"]["name"]
278
+ + author = data["author"]["display_name"]
279
+ + created_on = data["created_on"]
280
+ + reviewers = data.get("reviewers", [])
281
+ + reviewer_mentions = " ".join([f"@{r.get('nickname')}" for r in reviewers]) if reviewers else "None"
282
+ + pr_link = data["links"]["html"]["href"]
283
+ + changelog = data["summary"]["raw"]
284
+ +
285
+ +
286
+ + pr_report = f"""
287
+ +# [{args.repo}] PR #{args.pr}: {title}
288
+ +*{description}*
289
+ +
290
+ +**🌿 Branch Information:**
291
+ +- **Source Branch:** {source_branch} → **Target Branch:** {destination_branch}
292
+ +
293
+ +**👤 Người tạo:** {author}
294
+ +
295
+ +**📅 Thời gian tạo:** 2024-08-20 14:30:00 +07:00
296
+ +
297
+ +**👥 Reviewers:**
298
+ +{reviewer_mentions}
299
+ +
300
+ +**🔗 Link Pull Request:** [PR #{args.pr}: Implement user authentication system]({pr_link})
301
+ +
302
+ +---
303
+ +
304
+ +## 📝 Changelogs:
305
+ + """
306
+ +
307
+ + save_md_report("PR.md", pr_report)
308
+ +
309
+ +
310
+ +if __name__ == "__main__":
311
+ + main()
312
+ +
313
+ +
README.md CHANGED
@@ -7,4 +7,67 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
7
  pinned: false
8
  ---
9
 
10
+ # PR Webhook with Code Review Agent
11
+
12
+ A webhook service that integrates with a code review agent powered by Pydantic AI and enhanced with Logfire tracing.
13
+
14
+ ## Features
15
+
16
+ - **Code Review Agent**: Automated code review using Pydantic AI
17
+ - **Logfire Tracing**: Comprehensive tracing and monitoring of agent runs
18
+ - **Webhook Integration**: Handles pull request webhooks
19
+ - **Structured Output**: Generates detailed code review reports
20
+
21
+ ## Setup
22
+
23
+ ### Environment Variables
24
+
25
+ Create a `.env` file with the following variables:
26
+
27
+ ```bash
28
+ # Logfire Configuration
29
+ LOGFIRE_API_KEY=your_logfire_api_key_here
30
+
31
+ # Code Review Model
32
+ CODE_REVIEW_MODEL=google-gla:gemini-2.5-pro
33
+ ```
34
+
35
+ ### Installation
36
+
37
+ ```bash
38
+ pip install -r requirements.txt
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ ### Running the Agent
44
+
45
+ ```bash
46
+ python agent/code_review.py
47
+ ```
48
+
49
+ ### Using the Webhook
50
+
51
+ The service will automatically trace all agent runs with detailed metrics including:
52
+ - Execution time
53
+ - Input/output sizes
54
+ - Error tracking
55
+ - Performance metrics
56
+
57
+ ## Tracing Features
58
+
59
+ The agent now includes comprehensive tracing with Logfire:
60
+
61
+ - **Span Tracking**: Each major operation is wrapped in a tracing span
62
+ - **Event Logging**: Key milestones are logged as events
63
+ - **Error Handling**: Exceptions are automatically captured and traced
64
+ - **Performance Metrics**: Input/output sizes and execution times are tracked
65
+ - **Context Attributes**: Relevant metadata is attached to each span
66
+
67
+ ## Architecture
68
+
69
+ - `agent/code_review.py`: Main code review agent with Logfire tracing
70
+ - `index.py`: Webhook endpoint handler
71
+ - `tool.py`: Utility functions and webhook processing
72
+
73
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
agent/code_review.py CHANGED
@@ -10,9 +10,16 @@ from typing import List, Optional, Dict, Iterable, Tuple
10
  from pydantic import BaseModel, Field
11
  from pydantic_ai import Agent, RunContext
12
  from dotenv import load_dotenv
 
13
 
14
  load_dotenv()
15
 
 
 
 
 
 
 
16
  # =============================
17
  # Data models for structured output
18
  # =============================
@@ -350,17 +357,49 @@ async def review_paths(
350
  focus_areas: Optional[List[str]] = None,
351
  model: Optional[str] = None,
352
  ) -> CodeReviewResponse:
353
- files, dirs = gather_targets(paths)
354
-
355
- agent = code_review_agent if model is None else Agent(
356
- model=model,
357
- result_model=CodeReviewResponse,
358
- system_prompt=SYSTEM_PROMPT,
359
- )
360
-
361
- user_prompt = build_user_prompt(files, dirs, focus_areas or [])
362
- run = await agent.run(user_prompt)
363
- return run.data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
 
366
  async def review_code_string(
@@ -369,18 +408,45 @@ async def review_code_string(
369
  focus_areas: Optional[List[str]] = None,
370
  model: Optional[str] = None,
371
  ) -> CodeReviewResponse:
372
- agent = code_review_agent if model is None else Agent(
373
- model=model,
374
- result_model=CodeReviewResponse,
375
- system_prompt=SYSTEM_PROMPT,
376
- )
377
- prompt = (
378
- f"Review the following code ({filename}).\n\n" # noqa: E501
379
- f"Focus areas: {', '.join(focus_areas or []) or 'general quality'}.\n\n"
380
- f"{code}"
381
- )
382
- run = await agent.run(prompt)
383
- return run.data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
 
386
  # =============================
@@ -415,28 +481,76 @@ def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
415
 
416
 
417
  def main(argv: Optional[List[str]] = None) -> int:
418
- args = parse_args(argv)
419
- if not args.paths:
420
- print("No input paths provided. Nothing to review.")
421
- return 2
422
-
423
- result = asyncio.run(review_paths(args.paths, focus_areas=args.focus, model=args.model))
424
-
425
- md = render_markdown(result)
426
- if args.out:
427
- Path(args.out).write_text(md, encoding="utf-8")
428
- print(f"Saved review report to {args.out}")
429
- else:
430
- print(md)
431
- return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
 
434
  if __name__ == "__main__":
435
- # raise SystemExit(main())
436
- path = "DIFF.md"
437
- data = read_text_file(path)
438
- res = code_review_agent.run_sync("", deps = data)
439
-
440
- print(res.output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
 
 
10
  from pydantic import BaseModel, Field
11
  from pydantic_ai import Agent, RunContext
12
  from dotenv import load_dotenv
13
+ import logfire
14
 
15
  load_dotenv()
16
 
17
+ # =============================
18
+ # Logfire configuration for tracing
19
+ # =============================
20
+ logfire.configure(token = os.getenv('LOGFIRE_API_KEY'))
21
+ logfire.instrument_pydantic_ai()
22
+
23
  # =============================
24
  # Data models for structured output
25
  # =============================
 
357
  focus_areas: Optional[List[str]] = None,
358
  model: Optional[str] = None,
359
  ) -> CodeReviewResponse:
360
+ # Start tracing span
361
+ with logfire_client.span("review_paths", attributes={
362
+ "paths_count": len(paths),
363
+ "focus_areas": focus_areas or [],
364
+ "model": model or "default"
365
+ }) as span:
366
+ try:
367
+ files, dirs = gather_targets(paths)
368
+
369
+ # Log file analysis
370
+ span.add_event("files_analyzed", attributes={
371
+ "files_count": len(files),
372
+ "directories_count": len(dirs)
373
+ })
374
+
375
+ agent = code_review_agent if model is None else Agent(
376
+ model=model,
377
+ result_model=CodeReviewResponse,
378
+ system_prompt=systemt_prompt, # Use the system prompt directly
379
+ )
380
+
381
+ user_prompt = build_user_prompt(files, dirs, focus_areas or [])
382
+
383
+ # Log prompt generation
384
+ span.add_event("prompt_generated", attributes={
385
+ "prompt_length": len(user_prompt)
386
+ })
387
+
388
+ run = await agent.run(user_prompt)
389
+
390
+ # Log successful completion
391
+ span.add_event("review_completed", attributes={
392
+ "overall_score": run.data.overall_score,
393
+ "files_reviewed": len(run.data.files)
394
+ })
395
+
396
+ return run.data
397
+
398
+ except Exception as e:
399
+ # Log error
400
+ span.record_exception(e)
401
+ span.set_status(logfire.StatusCode.ERROR, str(e))
402
+ raise
403
 
404
 
405
  async def review_code_string(
 
408
  focus_areas: Optional[List[str]] = None,
409
  model: Optional[str] = None,
410
  ) -> CodeReviewResponse:
411
+ # Start tracing span
412
+ with logfire_client.span("review_code_string", attributes={
413
+ "filename": filename,
414
+ "code_length": len(code),
415
+ "focus_areas": focus_areas or [],
416
+ "model": model or "default"
417
+ }) as span:
418
+ try:
419
+ agent = code_review_agent if model is None else Agent(
420
+ model=model,
421
+ result_model=CodeReviewResponse,
422
+ system_prompt=systemt_prompt, # Use the system prompt directly
423
+ )
424
+ prompt = (
425
+ f"Review the following code ({filename}).\n\n" # noqa: E501
426
+ f"Focus areas: {', '.join(focus_areas or []) or 'general quality'}.\n\n"
427
+ f"{code}"
428
+ )
429
+
430
+ # Log prompt generation
431
+ span.add_event("prompt_generated", attributes={
432
+ "prompt_length": len(prompt)
433
+ })
434
+
435
+ run = await agent.run(prompt)
436
+
437
+ # Log successful completion
438
+ span.add_event("review_completed", attributes={
439
+ "overall_score": run.data.overall_score,
440
+ "files_reviewed": len(run.data.files)
441
+ })
442
+
443
+ return run.data
444
+
445
+ except Exception as e:
446
+ # Log error
447
+ span.record_exception(e)
448
+ span.set_status(logfire.StatusCode.ERROR, str(e))
449
+ raise
450
 
451
 
452
  # =============================
 
481
 
482
 
483
  def main(argv: Optional[List[str]] = None) -> int:
484
+ # Start tracing span for CLI execution
485
+ with logfire_client.span("cli_execution", attributes={
486
+ "argv": argv or []
487
+ }) as span:
488
+ try:
489
+ args = parse_args(argv)
490
+ if not args.paths:
491
+ span.add_event("no_paths_provided")
492
+ print("No input paths provided. Nothing to review.")
493
+ return 2
494
+
495
+ # Log CLI arguments
496
+ span.add_event("cli_args_parsed", attributes={
497
+ "paths_count": len(args.paths),
498
+ "focus_areas": args.focus,
499
+ "model": args.model,
500
+ "output_file": args.out
501
+ })
502
+
503
+ result = asyncio.run(review_paths(args.paths, focus_areas=args.focus, model=args.model))
504
+
505
+ md = render_markdown(result)
506
+ if args.out:
507
+ Path(args.out).write_text(md, encoding="utf-8")
508
+ span.add_event("report_saved", attributes={"output_file": args.out})
509
+ print(f"Saved review report to {args.out}")
510
+ else:
511
+ span.add_event("report_printed_to_console")
512
+ print(md)
513
+
514
+ span.add_event("cli_completed_successfully")
515
+ return 0
516
+
517
+ except Exception as e:
518
+ # Log error
519
+ span.record_exception(e)
520
+ span.set_status(logfire.StatusCode.ERROR, str(e))
521
+ raise
522
 
523
 
524
  if __name__ == "__main__":
525
+ # Start tracing span for test execution
526
+ # with logfire_client.span("test_execution", attributes={
527
+ # "test_type": "diff_review"
528
+ # }) as span:
529
+ # try:
530
+ # path = "DIFF.md"
531
+ # span.add_event("reading_diff_file", attributes={"file_path": path})
532
+
533
+ # data = read_text_file(path)
534
+ # span.add_event("diff_file_read", attributes={"content_length": len(data)})
535
+
536
+ # span.add_event("starting_agent_run")
537
+ # res = code_review_agent.run_sync("", deps=data)
538
+
539
+ # span.add_event("agent_run_completed", attributes={
540
+ # "output_length": len(res.output) if res.output else 0
541
+ # })
542
+
543
+ # print(res.output)
544
+ # span.add_event("test_completed_successfully")
545
+
546
+ # except Exception as e:
547
+ # # Log error
548
+ # span.record_exception(e)
549
+ # span.set_status(logfire.StatusCode.ERROR, str(e))
550
+ # raise
551
+
552
+ diff = read_text_file("DIFF.md")
553
+ data = code_review_agent.run_sync("", deps = DiffDeps(diff = diff))
554
+ print(data)
555
 
556
 
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  python-dotenv
2
  requests
3
  pydantic-ai
4
- flask
 
 
1
  python-dotenv
2
  requests
3
  pydantic-ai
4
+ flask
5
+ logfire
test_logfire.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script to verify Logfire tracing is working correctly
4
+ """
5
+
6
+ import os
7
+ import asyncio
8
+ from dotenv import load_dotenv
9
+ import logfire
10
+ # from agent.code_review import logfire_client
11
+
12
+ load_dotenv()
13
+
14
+ # async def test_logfire_tracing():
15
+ # """Test basic Logfire functionality"""
16
+
17
+ # # Test basic span creation
18
+ # with logfire_client.span("test_span", attributes={"test": "basic_functionality"}) as span:
19
+ # span.add_event("test_event", attributes={"message": "Hello from Logfire!"})
20
+ # print("✓ Basic span creation and event logging works")
21
+
22
+ # # Test error handling
23
+ # try:
24
+ # with logfire_client.span("error_test_span") as span:
25
+ # raise ValueError("Test error for tracing")
26
+ # except ValueError:
27
+ # print("✓ Error tracing works correctly")
28
+
29
+ # # Test nested spans
30
+ # with logfire_client.span("parent_span") as parent_span:
31
+ # parent_span.add_event("parent_event")
32
+
33
+ # with logfire_client.span("child_span") as child_span:
34
+ # child_span.add_event("child_event")
35
+ # print("✓ Nested spans work correctly")
36
+
37
+ # print("✓ All Logfire tests passed!")
38
+
39
+ # def test_sync_tracing():
40
+ # """Test synchronous tracing"""
41
+ # with logfire_client.span("sync_test") as span:
42
+ # span.add_event("sync_event", attributes={"type": "synchronous"})
43
+ # print("✓ Synchronous tracing works")
44
+
45
+ # # Test exception handling
46
+ # try:
47
+ # with logfire_client.span("sync_error_test") as span:
48
+ # raise RuntimeError("Sync test error")
49
+ # except RuntimeError:
50
+ # print("✓ Synchronous error tracing works")
51
+
52
+ # if __name__ == "__main__":
53
+ # print("Testing Logfire tracing functionality...")
54
+ # print(f"Logfire API Key configured: {'Yes' if os.getenv('LOGFIRE_API_KEY') else 'No'}")
55
+ # print()
56
+
57
+ # # Test sync tracing
58
+ # test_sync_tracing()
59
+
60
+ # # Test async tracing
61
+ # asyncio.run(test_logfire_tracing())
62
+
63
+ # print("\n🎉 Logfire tracing is working correctly!")
64
+
65
+ from pathlib import Path
66
+ logfire.configure(token = os.getenv('LOGFIRE_API_KEY'))
67
+
68
+ cwd = Path.cwd()
69
+ total_size = 0
70
+
71
+ with logfire.span('counting size of {cwd=}', cwd=cwd):
72
+ for path in cwd.iterdir():
73
+ if path.is_file():
74
+ with logfire.span('reading {path}', path=path.relative_to(cwd)):
75
+ total_size += len(path.read_bytes())
76
+
77
+ logfire.info('total size of {cwd} is {size} bytes', cwd=cwd, size=total_size)
78
+
tool.py CHANGED
@@ -128,7 +128,7 @@ def send_slack_message(
128
 
129
  # Ưu tiên gửi qua Incoming Webhook nếu có
130
  if resolved_webhook_url:
131
- payload: Dict[str, Any] = {"text": message_text}
132
  if blocks is not None:
133
  payload["blocks"] = blocks
134
 
 
128
 
129
  # Ưu tiên gửi qua Incoming Webhook nếu có
130
  if resolved_webhook_url:
131
+ payload: Dict[str, Any] = {"text": message_text, "type": "mrkdwn"}
132
  if blocks is not None:
133
  payload["blocks"] = blocks
134