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

Ai sp estimation #336

Merged
merged 5 commits into from
Mar 10, 2025
Merged
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
4 changes: 3 additions & 1 deletion gurubu-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ JIRA_PROJECT_KEY_THREE=
JIRA_PROJECT_KEY_FOUR=
JIRA_DEFAULT_BOARD_ID=
JIRA_DEFAULT_ASSIGNEES=
P_GATEWAY_URL=
P_GATEWAY_URL=
OPENAI_API_KEY=
OPENAI_ASSISTANT_ID=
4 changes: 4 additions & 0 deletions gurubu-backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ ARG JIRA_PROJECT_KEY_FOUR
ARG JIRA_DEFAULT_BOARD_ID
ARG JIRA_DEFAULT_ASSIGNEES
ARG P_GATEWAY_URL
ARG OPENAI_ASSISTANT_ID
ARG OPENAI_API_KEY

ENV CLIENT_URL=$CLIENT_URL
ENV JIRA_BASE_URL=$JIRA_BASE_URL
Expand All @@ -33,6 +35,8 @@ ENV JIRA_PROJECT_KEY_FOUR=$JIRA_PROJECT_KEY_FOUR
ENV JIRA_DEFAULT_BOARD_ID=$JIRA_DEFAULT_BOARD_ID
ENV JIRA_DEFAULT_ASSIGNEES=$JIRA_DEFAULT_ASSIGNEES
ENV P_GATEWAY_URL=$P_GATEWAY_URL
ENV OPENAI_ASSISTANT_ID=$OPENAI_ASSISTANT_ID
ENV OPENAI_API_KEY=$OPENAI_API_KEY

RUN yarn build

Expand Down
42 changes: 42 additions & 0 deletions gurubu-backend/controllers/storyPointEstimationController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const openaiService = require("../services/openaiService");

exports.estimateStoryPoint = async (req, res) => {
try {
const { boardName, issueSummary, issueDescription, threadId } = req.body;

if(!boardName) {
return res.status(400).json({ error: "Board name is required" });
}

if (!issueSummary) {
return res.status(400).json({ error: "Issue Summary is required" });
}

if (!issueDescription) {
return res.status(400).json({ error: "Issue Description is required" });
}

const assistantId = process.env.OPENAI_ASSISTANT_ID;

const message = createStoryPointPrompt(issueSummary, issueDescription);

const response = await openaiService.askAssistant(assistantId, message, threadId);

res.json(response);
} catch (error) {
console.error("Error estimating story point:", error);
res.status(500).json({ error: error.message || "Failed to estimate story point" });
}
};

function createStoryPointPrompt(issueSummary, issueDescription, maxSp = 13) {
return `Lütfen aşağıdaki issue için sadece bir storypoint değeri döndür.
Cevabında kesinlikle başka hiçbir açıklama, metin veya karakter olmasın, sadece rakam olsun.
Sadece sayısal değeri döndür. Örneğin: 5

Bir işe verilebilecek minimum SP değeri 1, maksimum SP değeri ${maxSp}.
Fibonacci serisine göre SP değerleri: 1, 2, 3, 5, 8, 13 ... ... ... olabilir.

Issue Summary: ${issueSummary}
Issue Description: ${issueDescription}`;
}
6 changes: 5 additions & 1 deletion gurubu-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
"license": "MIT",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "nodemon server.js"
"start": "nodemon server.js",
"test": "jest"
},
"dependencies": {
"axios": "^1.7.9",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"openai": "^4.86.2",
"redis": "^4.6.10",
"socket.io": "^4.7.2",
"uuid": "^9.0.1"
Expand All @@ -21,7 +23,9 @@
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"babel-loader": "^9.1.3",
"jest": "^29.7.0",
"nodemon": "^3.0.1",
"supertest": "^7.0.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
Expand Down
7 changes: 7 additions & 0 deletions gurubu-backend/routes/storyPointRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const express = require("express");
const router = express.Router();
const storyPointController = require("../controllers/storyPointEstimationController");

router.post("/estimate", storyPointController.estimateStoryPoint);

module.exports = router;
4 changes: 3 additions & 1 deletion gurubu-backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ const roomRoutes = require("./routes/roomRoutes");
const healthCheckRoute = require("./routes/healthCheckRoute");
const jiraRoutes = require("./routes/jiraRoutes");
const pRoutes = require("./routes/pRoutes");
const storyPointRoutes = require("./routes/storyPointRoutes");

app.use("/room", cors(corsOptions), roomRoutes);
app.use("/healthcheck", cors(corsOptions), healthCheckRoute);
app.use("/jira", cors(corsOptions), jiraRoutes);
app.use("/p", cors(corsOptions), pRoutes);
app.use("/storypoint", cors(corsOptions), storyPointRoutes);

const PORT = process.env.PORT || 5000;
const PORT = process.env.PORT || 5001;
const server = app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Expand Down
80 changes: 80 additions & 0 deletions gurubu-backend/services/openaiService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const { OpenAI } = require("openai");
const dotenv = require("dotenv");

dotenv.config();

class OpenAIService {
constructor() {
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}


async askAssistant(assistantId, message, threadId = null) {
try {

let thread;
if (!threadId) {
thread = await this.openai.beta.threads.create();
threadId = thread.id;
} else {
threadId = threadId;
}

await this.openai.beta.threads.messages.create(threadId, {
role: "user",
content: message,
});

const run = await this.openai.beta.threads.runs.create(threadId, {
assistant_id: assistantId,
});

let runStatus = await this.openai.beta.threads.runs.retrieve(
threadId,
run.id
);

while (runStatus.status !== "completed") {
if (runStatus.status === "failed") {
throw new Error(`Run failed with error: ${runStatus.last_error}`);
}

if (["expired", "cancelled"].includes(runStatus.status)) {
throw new Error(`Run ${runStatus.status}`);
}

await new Promise((resolve) => setTimeout(resolve, 1000));
runStatus = await this.openai.beta.threads.runs.retrieve(
threadId,
run.id
);
}

const messages = await this.openai.beta.threads.messages.list(threadId);

const assistantMessages = messages.data.filter(
(msg) => msg.role === "assistant"
);

if (assistantMessages.length === 0) {
return { response: "No response from assistant" };
}

const latestMessage = assistantMessages[0];

const textContent = latestMessage.content
.filter((content) => content.type === "text")
.map((content) => content.text.value)
.join("\n");

return { response: textContent, threadId: threadId };
} catch (error) {
console.error("Error in askAssistant:", error);
throw new Error(`Failed to get response from assistant: ${error.message}`);
}
}
}

module.exports = new OpenAIService();
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const request = require('supertest');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cors = require('cors');

require('dotenv').config();

app.use(bodyParser.json());
app.use(cors());

const storyPointRoutes = require('../../routes/storyPointRoutes');
app.use('/storypoint', storyPointRoutes);

describe('Story Point Estimation Controller', () => {
jest.setTimeout(60000); // 60 saniye timeout

// OpenAI API'ye erişim için gerekli ortam değişkenlerini kontrol et
beforeAll(() => {
if (!process.env.OPENAI_API_KEY) {
console.warn('OPENAI_API_KEY ortam değişkeni ayarlanmamış. Test başarısız olabilir.');
}

if (!process.env.OPENAI_ASSISTANT_ID) {
console.warn('OPENAI_ASSISTANT_ID ortam değişkeni ayarlanmamış. Test başarısız olabilir.');
}
});

test('Bir issue için story point tahmini yapabilmeli', async () => {
const testData = {
boardName: 'Test board',
issueSummary: 'Kullanıcı girişi sayfası oluşturulmalı',
issueDescription: 'Kullanıcıların sisteme giriş yapabileceği bir sayfa oluşturulmalı. Sayfa email ve şifre alanı içermeli.'
};

const response = await request(app)
.post('/storypoint/estimate')
.send(testData);

expect(response.status).toBe(200);
expect(response.body).toHaveProperty('response');
expect(response.body).toHaveProperty('threadId');

console.log('OpenAI yanıtı:', response.body.response);

const responseStr = response.body.response.trim();
const isNumeric = /^\d+$/.test(responseStr);
expect(isNumeric).toBe(true);

const validSPValues = [1, 2, 3, 5, 8, 13];
const responseNum = parseInt(responseStr, 10);
expect(validSPValues.includes(responseNum)).toBe(true);

// Thread ID'yi sakla
const threadId = response.body.threadId;
console.log('Thread ID:', threadId);


if (threadId) {
const followUpData = {
boardName: 'Test board',
issueSummary: 'Kullanıcı girişi sayfası güncellenmeli',
issueDescription: 'Mevcut giriş sayfasına şifre sıfırlama özelliği eklenmeli.',
threadId: threadId
};

const followUpResponse = await request(app)
.post('/storypoint/estimate')
.send(followUpData);

expect(followUpResponse.status).toBe(200);
expect(followUpResponse.body).toHaveProperty('response');
expect(followUpResponse.body).toHaveProperty('threadId');
expect(followUpResponse.body.threadId).toBe(threadId);

console.log('Takip sorusu yanıtı:', followUpResponse.body.response);

const followUpResponseStr = followUpResponse.body.response.trim();
const isFollowUpNumeric = /^\d+$/.test(followUpResponseStr);
expect(isFollowUpNumeric).toBe(true);

const validSPValues = [1, 2, 3, 5, 8, 13];
const followUpResponseNum = parseInt(followUpResponseStr, 10);
expect(validSPValues.includes(followUpResponseNum)).toBe(true);
}
});

});
Loading
Loading