Skip to content

Commit 59cacb8

Browse files
feat(#82): Enhance refactoring logs and add error handling in Critic agent (#107)
1 parent 06010f7 commit 59cacb8

File tree

18 files changed

+295
-281
lines changed

18 files changed

+295
-281
lines changed

internal/client/refrax_client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func (c *RefraxClient) Refactor(proj domain.Project) (domain.Project, error) {
138138
<-rvwr.Ready()
139139

140140
log.Info("all servers are ready: facilitator %d, critic %d, fixer %d, reviewer %d", facilitatorPort, criticPort, fixerPort, reviewerPort)
141-
log.Info("begin refactoring")
141+
log.Info("begin refactoring for project %s with %d classes", proj, len(classes))
142142
ch := make(chan refactoring, len(classes))
143143
go refactor(fclttor, proj, c.params.MaxSize, ch)
144144
for range len(classes) {
@@ -184,6 +184,7 @@ func refactor(f domain.Facilitator, p domain.Project, size int, ch chan<- refact
184184
}
185185
artifacts, err := f.Refactor(&job)
186186
if err != nil {
187+
log.Error("failed to refactor project %s: %v", p, err)
187188
ch <- refactoring{err: fmt.Errorf("failed to refactor project %s: %w", p, err)}
188189
close(ch)
189190
return

internal/client/refrax_client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestRefraxClient_PrintsStatsIfEnabled(t *testing.T) {
3636
out := bytes.Buffer{}
3737
params.Log = NewSyncWriter(io.Writer(&out))
3838
client := NewRefraxClient(params)
39-
_, err := client.Refactor(domain.NewInMemory(domain.NewClass("Foo.java", ".", "abstract class Foo {}")))
39+
_, err := client.Refactor(domain.NewInMemory(domain.NewInMemoryClass("Foo.java", ".", "abstract class Foo {}")))
4040
assert.NoError(t, err)
4141
assert.Contains(t, out.String(), "Total LLM messages asked", "Expected total messages asked to be logged")
4242
}

internal/critic/agent.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package critic
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/cqfn/refrax/internal/brain"
8+
"github.com/cqfn/refrax/internal/domain"
9+
"github.com/cqfn/refrax/internal/log"
10+
"github.com/cqfn/refrax/internal/tool"
11+
)
12+
13+
type agent struct {
14+
brain brain.Brain
15+
log log.Logger
16+
tools []tool.Tool
17+
}
18+
19+
const notFound = "No suggestions found"
20+
21+
const prompt = `Analyze the following Java code:
22+
23+
{{code}}
24+
25+
Identify possible improvements or flaws such as:
26+
27+
* grammar and spelling mistakes in comments,
28+
* variables that can be inlined or removed without changing functionality,
29+
* unnecessary comments inside methods,
30+
* redundant code.
31+
32+
Don't suggest any changes that would alter the functionality of the code.
33+
Don't suggest any changes that would require moving code parts between files (like extract class or extract an interface).
34+
Don't suggest method renaming or class renaming.
35+
36+
Keep in mind the following imperfections with Java code, identified by automated static analysis system:
37+
38+
{{imperfections}}
39+
40+
Respond with a few most relevant and important suggestion for improvement, formatted as a few lines of text.
41+
If there are no suggestions or they are insignificant, respond with "{{not-found}}".
42+
Do not include any explanations, summaries, or extra text.
43+
`
44+
45+
// Review sends the provided Java class to the Critic for analysis and returns suggested improvements.
46+
func (c *agent) Review(job *domain.Job) (*domain.Artifacts, error) {
47+
class := job.Classes[0]
48+
c.log.Info("received class %q for analysis", class.Name())
49+
replacer := strings.NewReplacer(
50+
"{{code}}", class.Content(),
51+
"{{imperfections}}", tool.NewCombined(c.tools...).Imperfections(),
52+
"{{not-found}}", notFound,
53+
)
54+
answer, err := c.brain.Ask(replacer.Replace(prompt))
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to get answer from brain: %w", err)
57+
}
58+
suggestions := c.associated(parseAnswer(answer), class.Path())
59+
logSuggestions(c.log, suggestions)
60+
artifacts := domain.Artifacts{
61+
Descr: &domain.Description{
62+
Text: fmt.Sprintf("Critique for class %s", class.Name()),
63+
},
64+
Suggestions: suggestions,
65+
}
66+
return &artifacts, nil
67+
}
68+
69+
func logSuggestions(logger log.Logger, suggestions []domain.Suggestion) {
70+
for i, suggestion := range suggestions {
71+
logger.Info("#%d: %s", i+1, suggestion)
72+
}
73+
}
74+
75+
func parseAnswer(answer string) []string {
76+
lines := strings.Split(strings.TrimSpace(answer), "\n")
77+
var suggestions []string
78+
for _, line := range lines {
79+
suggestion := strings.TrimSpace(line)
80+
if suggestion != "" {
81+
suggestions = append(suggestions, suggestion)
82+
}
83+
}
84+
return suggestions
85+
}
86+
87+
func (a *agent) associated(suggestions []string, class string) []domain.Suggestion {
88+
res := make([]domain.Suggestion, 0)
89+
for i, suggestion := range suggestions {
90+
if strings.EqualFold(suggestion, notFound) {
91+
a.log.Info("no suggestions found for the class #%d: %s", i+1, class)
92+
} else {
93+
res = append(res, *domain.NewSuggestion(suggestion, class))
94+
}
95+
}
96+
a.log.Info("total suggestions associated with class %s: %d", class, len(res))
97+
return res
98+
}

internal/critic/server.go

Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"fmt"
77
"net/http"
8-
"strings"
98

109
"github.com/cqfn/refrax/internal/brain"
1110
"github.com/cqfn/refrax/internal/domain"
@@ -17,48 +16,20 @@ import (
1716
// Critic represents the main struct responsible for analyzing code critiques.
1817
type Critic struct {
1918
server protocol.Server
20-
brain brain.Brain
2119
log log.Logger
2220
port int
23-
tools []tool.Tool
21+
agent *agent
2422
}
2523

26-
const notFound = "No suggestions found"
27-
28-
const prompt = `Analyze the following Java code:
29-
30-
{{code}}
31-
32-
Identify possible improvements or flaws such as:
33-
34-
* grammar and spelling mistakes in comments,
35-
* variables that can be inlined or removed without changing functionality,
36-
* unnecessary comments inside methods,
37-
* redundant code.
38-
39-
Don't suggest any changes that would alter the functionality of the code.
40-
Don't suggest any changes that would require moving code parts between files (like extract class or extract an interface).
41-
Don't suggest method renaming or class renaming.
42-
43-
Keep in mind the following imperfections with Java code, identified by automated static analysis system:
44-
45-
{{imperfections}}
46-
47-
Respond with a few most relevant and important suggestion for improvement, formatted as a few lines of text.
48-
If there are no suggestions or they are insignificant, respond with "{{not-found}}".
49-
Do not include any explanations, summaries, or extra text.
50-
`
51-
5224
// NewCritic creates and initializes a new instance of Critic.
5325
func NewCritic(ai brain.Brain, port int, tools ...tool.Tool) *Critic {
5426
logger := log.NewPrefixed("critic", log.NewColored(log.Default(), log.Cyan))
5527
server := protocol.NewServer(agentCard(port), port)
5628
critic := &Critic{
5729
server: server,
58-
brain: ai,
5930
log: logger,
6031
port: port,
61-
tools: tools,
32+
agent: &agent{brain: ai, log: logger, tools: tools},
6233
}
6334
server.MsgHandler(critic.think)
6435
critic.log.Debug("preparing the Critic server on port %d with ai provider %s", port, ai)
@@ -122,62 +93,8 @@ func (c *Critic) thinkLong(m *protocol.Message) (*protocol.Message, error) {
12293
if err != nil {
12394
return nil, fmt.Errorf("failed to parse task from message: %w", err)
12495
}
125-
class := tsk.Classes[0]
126-
c.log.Info("received class %q for analysis", class.Name())
127-
replacer := strings.NewReplacer(
128-
"{{code}}", class.Content(),
129-
"{{imperfections}}", tool.NewCombined(c.tools...).Imperfections(),
130-
"{{not-found}}", notFound,
131-
)
132-
answer, err := c.brain.Ask(replacer.Replace(prompt))
133-
if err != nil {
134-
return nil, fmt.Errorf("failed to get answer from brain: %w", err)
135-
}
136-
suggestions := parseAnswer(answer)
137-
suggestions = associated(suggestions, class.Path())
138-
logSuggestions(c.log, suggestions)
139-
all := make([]domain.Suggestion, 0, len(suggestions))
140-
for _, suggestion := range suggestions {
141-
if strings.EqualFold(suggestion, notFound) {
142-
c.log.Info("no suggestions found for class %s", class.Name())
143-
continue
144-
}
145-
s := domain.NewSuggestion(suggestion)
146-
all = append(all, s)
147-
}
148-
artifacts := domain.Artifacts{
149-
Descr: &domain.Description{
150-
Text: fmt.Sprintf("Critique for class %s", class.Name()),
151-
},
152-
153-
Suggestions: all,
154-
}
155-
return artifacts.Marshal().Message, nil
156-
}
157-
158-
func logSuggestions(logger log.Logger, suggestions []string) {
159-
for i, suggestion := range suggestions {
160-
logger.Info("suggestion #%d: %s", i+1, suggestion)
161-
}
162-
}
163-
164-
func parseAnswer(answer string) []string {
165-
lines := strings.Split(strings.TrimSpace(answer), "\n")
166-
var suggestions []string
167-
for _, line := range lines {
168-
suggestion := strings.TrimSpace(line)
169-
if suggestion != "" {
170-
suggestions = append(suggestions, suggestion)
171-
}
172-
}
173-
return suggestions
174-
}
175-
176-
func associated(suggestions []string, classPath string) []string {
177-
for i, suggestion := range suggestions {
178-
suggestions[i] = fmt.Sprintf("%s: %s", classPath, suggestion)
179-
}
180-
return suggestions
96+
artifacts, err := c.agent.Review(tsk)
97+
return artifacts.Marshal().Message, err
18198
}
18299

183100
func agentCard(port int) *protocol.AgentCard {

internal/critic/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestNewCritic_Success(t *testing.T) {
6161

6262
critic := NewCritic(ai, 18081)
6363
require.NotNil(t, critic)
64-
assert.Equal(t, ai, critic.brain)
64+
assert.Equal(t, ai, critic.agent.brain)
6565
}
6666

6767
func TestCriticStart_Success(t *testing.T) {

internal/domain/a2a.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func UnmarshalArtifacts(msg *protocol.Message) (*Artifacts, error) {
4141
if err != nil {
4242
return nil, fmt.Errorf("failed to unmarshal suggestion: %w", err)
4343
}
44-
suggestions = append(suggestions, s)
44+
suggestions = append(suggestions, *s)
4545
default:
4646
return nil, fmt.Errorf("unknown part type %s", t)
4747
}
@@ -85,7 +85,7 @@ func UnmarshalJob(msg *protocol.Message) (*Job, error) {
8585
if err != nil {
8686
return nil, fmt.Errorf("failed to unmarshal suggestion: %w", err)
8787
}
88-
suggestions = append(suggestions, s)
88+
suggestions = append(suggestions, *s)
8989
default:
9090
return nil, fmt.Errorf("unknown part type %s", t)
9191
}
@@ -96,9 +96,26 @@ func UnmarshalJob(msg *protocol.Message) (*Job, error) {
9696
return job, nil
9797
}
9898

99-
func UnmarshalSuggestion(part protocol.Part) (Suggestion, error) {
99+
func UnmarshalOldSuggestion(part protocol.Part) (OldSuggestion, error) {
100100
if part.PartKind() == protocol.PartKindText {
101-
suggestion := NewSuggestion(part.(*protocol.TextPart).Text)
101+
suggestion := NewOldSuggestion(part.(*protocol.TextPart).Text)
102+
return suggestion, nil
103+
}
104+
return nil, fmt.Errorf("expected text part for suggestion, got %s", part.PartKind())
105+
}
106+
107+
func UnmarshalSuggestion(part protocol.Part) (*Suggestion, error) {
108+
if part.PartKind() == protocol.PartKindText {
109+
tp := part.(*protocol.TextPart)
110+
text := tp.Text
111+
path, ok := tp.Metadata()["class-path"].(string)
112+
if !ok {
113+
return nil, fmt.Errorf("missing or invalid class-path metadata in suggestion part")
114+
}
115+
suggestion := &Suggestion{
116+
ClassPath: path,
117+
Text: text,
118+
}
102119
return suggestion, nil
103120
}
104121
return nil, fmt.Errorf("expected text part for suggestion, got %s", part.PartKind())
@@ -114,7 +131,7 @@ func UnmarshalClass(part protocol.Part) (Class, error) {
114131
return nil, fmt.Errorf("failed to decode file part: %w", err)
115132
}
116133
meta := f.Metadata()
117-
return NewClass(
134+
return NewInMemoryClass(
118135
fmt.Sprintf("%v", meta["class-name"]),
119136
fmt.Sprintf("%v", meta["class-path"]),
120137
decoded,
@@ -148,10 +165,7 @@ func (j *Job) Marshal() *protocol.MessageSendParams {
148165
}
149166
if len(j.Suggestions) > 0 {
150167
for _, suggestion := range j.Suggestions {
151-
if suggestion == nil {
152-
continue
153-
}
154-
msg.AddPart(MarshalSuggestion(suggestion))
168+
msg.AddPart(suggestion.Marshal())
155169
}
156170
}
157171
if len(j.Examples) > 0 {
@@ -180,10 +194,7 @@ func (a *Artifacts) Marshal() *protocol.MessageSendParams {
180194
}
181195
if len(a.Suggestions) > 0 {
182196
for _, suggestion := range a.Suggestions {
183-
if suggestion == nil {
184-
continue
185-
}
186-
msg.AddPart(MarshalSuggestion(suggestion))
197+
msg.AddPart(suggestion.Marshal())
187198
}
188199
}
189200
return protocol.NewMessageSendParams().WithMessage(msg)
@@ -197,13 +208,20 @@ func (d *Description) Marshal() protocol.Part {
197208
return part
198209
}
199210

211+
func (s *Suggestion) Marshal() protocol.Part {
212+
part := protocol.NewText(s.Text).
213+
WithMetadata("class-path", s.ClassPath).
214+
WithMetadata("type", typeSuggestion)
215+
return part
216+
}
217+
200218
func MarshalClass(c Class, t string) protocol.Part {
201219
return protocol.NewFileBytes([]byte(c.Content())).
202220
WithMetadata("type", t).
203221
WithMetadata("class-name", c.Name()).
204222
WithMetadata("class-path", c.Path())
205223
}
206224

207-
func MarshalSuggestion(s Suggestion) protocol.Part {
225+
func MarshalSuggestion(s OldSuggestion) protocol.Part {
208226
return protocol.NewText(s.Text()).WithMetadata("type", typeSuggestion)
209227
}

internal/domain/a2a_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ func TestMarshalAndUnmarshalJob(t *testing.T) {
1414
Meta: map[string]any{"key": "value"},
1515
},
1616
Classes: []Class{
17-
NewClass("TestClass", "test/path/TestClass.java", "public class TestClass {}"),
18-
NewClass("AnotherClass", "test/path/AnotherClass.java", "public class AnotherClass {}"),
17+
NewInMemoryClass("TestClass", "test/path/TestClass.java", "public class TestClass {}"),
18+
NewInMemoryClass("AnotherClass", "test/path/AnotherClass.java", "public class AnotherClass {}"),
1919
},
2020
Examples: []Class{
21-
NewClass("ExampleClass", "test/path/ExampleClass.java", "public class ExampleClass {}"),
21+
NewInMemoryClass("ExampleClass", "test/path/ExampleClass.java", "public class ExampleClass {}"),
2222
},
2323
Suggestions: []Suggestion{
24-
NewSuggestion("Improve TestClass"),
25-
NewSuggestion("Add documentation to AnotherClass"),
26-
NewSuggestion("Refactor ExampleClass"),
24+
*NewSuggestion("Improve TestClass", "test/path/TestClass.java"),
25+
*NewSuggestion("Add documentation to AnotherClass", "test/path/AnotherClass.java"),
26+
*NewSuggestion("Refactor ExampleClass", "test/path/ExampleClass.java"),
2727
},
2828
}
2929
after, err := UnmarshalJob(before.Marshal().Message)

0 commit comments

Comments
 (0)