Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/integrate gemini insights #136

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 70 additions & 5 deletions pkg/api/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package handlers

import (
"bytes"
"context"
"errors"
"fmt"

"github.com/guidewire/fern-reporter/config"
"github.com/guidewire/fern-reporter/pkg/models"
"github.com/guidewire/fern-reporter/pkg/utils"

"log"
"net/http"
"os"
"strconv"
"time"

"github.com/gin-gonic/gin"
"github.com/google/generative-ai-go/genai"

Check failure on line 15 in pkg/api/handlers/handlers.go

View workflow job for this annotation

GitHub Actions / unit-tests

no required module provides package github.com/google/generative-ai-go/genai; to add it:
"google.golang.org/api/option"

Check failure on line 16 in pkg/api/handlers/handlers.go

View workflow job for this annotation

GitHub Actions / unit-tests

missing go.sum entry for module providing package google.golang.org/api/option (imported by github.com/guidewire/fern-reporter/pkg/api/handlers); to add:
"gorm.io/gorm"

"github.com/guidewire/fern-reporter/config"
"github.com/guidewire/fern-reporter/pkg/models"
"github.com/guidewire/fern-reporter/pkg/utils"
)

type Handler struct {
Expand Down Expand Up @@ -243,3 +247,64 @@
"message": "Fern Reporter is running!",
})
}

func (h *Handler) GetGeminiInsights(c *gin.Context) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this! It would be helpful to expand the data to be able to include more than one test run at a time (such as providing all the test runs from the same project), so that the model has more context to work with.

var testRunData struct {
ProjectName string `json:"projectName"`
TestName string `json:"testName"`
Status string `json:"status"`
Duration string `json:"duration"`
}

if err := c.ShouldBindJSON(&testRunData); err != nil {
log.Printf("Error binding JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}

prompt := fmt.Sprintf("Analyze this test run data and provide insights:\nProject: %s\nTest: %s\nStatus: %s\nDuration: %s",
testRunData.ProjectName, testRunData.TestName, testRunData.Status, testRunData.Duration)

response, err := callGeminiAPI(prompt)
if err != nil {
log.Printf("Error calling Gemini API: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get insights from Gemini: %v", err)})
return
}

c.JSON(http.StatusOK, gin.H{"insights": response})
}

func callGeminiAPI(prompt string) (string, error) {
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return "", fmt.Errorf("gemini API key is not set")
}

ctx := context.Background()
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return "", fmt.Errorf("failed to create Gemini client: %v", err)
}
defer client.Close()

model := client.GenerativeModel("gemini-pro")
response, err := model.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
return "", fmt.Errorf("failed to generate content: %v", err)
}

return printResponse(response), nil
}

func printResponse(resp *genai.GenerateContentResponse) string {
var buf bytes.Buffer
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
fmt.Fprintln(&buf, part)
}
}
}
return buf.String()
}
5 changes: 3 additions & 2 deletions pkg/api/routers/routers.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package routers

import (
"github.com/gin-gonic/gin"

"github.com/guidewire/fern-reporter/config"
"github.com/guidewire/fern-reporter/pkg/api/handlers"
"github.com/guidewire/fern-reporter/pkg/auth"
"github.com/guidewire/fern-reporter/pkg/db"

"github.com/gin-gonic/gin"
)

var (
Expand Down Expand Up @@ -66,5 +66,6 @@ func RegisterRouters(router *gin.Engine) {
insights := router.Group("/insights")
{
insights.GET("/:name", handler.ReportTestInsights)
insights.POST("/gemini", handler.GetGeminiInsights)
}
}
1 change: 1 addition & 0 deletions pkg/db/migrations/000001_create_test_run_table.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ CREATE TABLE public.test_runs (
test_project_name text,
start_time timestamp with time zone,
end_time timestamp with time zone,
enable_gemini_insights boolean NOT NULL DEFAULT true,
PRIMARY KEY(id, test_seed)
);

13 changes: 7 additions & 6 deletions pkg/models/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ type TimeLog struct {
}

type TestRun struct {
ID uint64 `json:"id" gorm:"primaryKey"`
TestProjectName string `json:"test_project_name"`
TestSeed uint64 `json:"test_seed"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
SuiteRuns []SuiteRun `json:"suite_runs" gorm:"foreignKey:TestRunID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ID uint64 `json:"id" gorm:"primaryKey"`
TestProjectName string `json:"test_project_name"`
TestSeed uint64 `json:"test_seed"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
SuiteRuns []SuiteRun `json:"suite_runs" gorm:"foreignKey:TestRunID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
EnableGeminiInsights bool `json:"enable_gemini_insights" gorm:"default:true"`
}

type SuiteRun struct {
Expand Down
106 changes: 105 additions & 1 deletion pkg/views/test_runs.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,44 @@
.table td {
word-wrap: break-word;
}

.gemini-insights-btn {
margin-left: 10px;
}

#geminiInsightsModal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}

.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}

.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}

.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -109,7 +147,12 @@ <h1 class="title is-3 has-text-centered has-background-primary has-text-white p-
<td class="test-name">{{ $specRun.SpecDescription }}</td>
<td class="test-status">{{ $specRun.Status}}</td>
<td class="test-duration">{{ CalculateDuration $specRun.StartTime $specRun.EndTime }}</td>
<td><button class="button is-info insights-btn" data-insights-url="/insights/{{ $testRun.TestProjectName }}">Insights</button></td>
<td>
<button class="button is-info insights-btn" data-insights-url="/insights/{{ $testRun.TestProjectName }}">Insights</button>
{{if $testRun.EnableGeminiInsights}}
<button class="button is-primary gemini-insights-btn" data-id="{{ $testRun.ID }}" data-project="{{ $testRun.TestProjectName }}" data-test="{{ $specRun.SpecDescription }}" data-status="{{ $specRun.Status }}" data-duration="{{ CalculateDuration $specRun.StartTime $specRun.EndTime }}">Gemini Insights</button>
{{end}}
</td>
<td>
{{ $tags := $specRun.Tags }}
{{ range $tag := $tags}}
Expand All @@ -130,6 +173,16 @@ <h1 class="title is-3 has-text-centered has-background-primary has-text-white p-
</tbody>
</table>
</div>

<!-- Gemini Insights Modal -->
<div id="geminiInsightsModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Gemini Insights</h2>
<p id="geminiInsightsContent"></p>
</div>
</div>

<script>
function filterTests(status) {
const testRows = document.querySelectorAll('.test-row');
Expand Down Expand Up @@ -169,7 +222,58 @@ <h1 class="title is-3 has-text-centered has-background-primary has-text-white p-
const insightsUrl = event.target.getAttribute('data-insights-url');
window.open(insightsUrl, '_blank');
});

row.querySelector('.gemini-insights-btn').addEventListener('click', (event) => {
event.stopPropagation();
const button = event.target;
const projectName = button.getAttribute('data-project');
const testName = button.getAttribute('data-test');
const status = button.getAttribute('data-status');
const duration = button.getAttribute('data-duration');

fetch('/insights/gemini', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
projectName: projectName,
testName: testName,
status: status,
duration: duration
}),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Success:', data);
document.getElementById('geminiInsightsContent').textContent = data.insights;
document.getElementById('geminiInsightsModal').style.display = 'block';
})
.catch(error => {
console.error('Error:', error);
alert('Failed to get Gemini insights. Please try again later.');
});
});
});

// Close modal when clicking on the close button or outside the modal
const modal = document.getElementById('geminiInsightsModal');
const closeBtn = document.querySelector('.close');

closeBtn.onclick = function() {
modal.style.display = 'none';
}

window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = 'none';
}
}
});
</script>
</body>
Expand Down
Loading