Skip to content

Commit c321ae5

Browse files
Merge pull request #336 from Trendyol/ai-sp-estimation
Ai sp estimation
2 parents b26ddaf + 039fff4 commit c321ae5

9 files changed

+2198
-16
lines changed

gurubu-backend/.env.example

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ JIRA_PROJECT_KEY_THREE=
88
JIRA_PROJECT_KEY_FOUR=
99
JIRA_DEFAULT_BOARD_ID=
1010
JIRA_DEFAULT_ASSIGNEES=
11-
P_GATEWAY_URL=
11+
P_GATEWAY_URL=
12+
OPENAI_API_KEY=
13+
OPENAI_ASSISTANT_ID=

gurubu-backend/Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ ARG JIRA_PROJECT_KEY_FOUR
2121
ARG JIRA_DEFAULT_BOARD_ID
2222
ARG JIRA_DEFAULT_ASSIGNEES
2323
ARG P_GATEWAY_URL
24+
ARG OPENAI_ASSISTANT_ID
25+
ARG OPENAI_API_KEY
2426

2527
ENV CLIENT_URL=$CLIENT_URL
2628
ENV JIRA_BASE_URL=$JIRA_BASE_URL
@@ -33,6 +35,8 @@ ENV JIRA_PROJECT_KEY_FOUR=$JIRA_PROJECT_KEY_FOUR
3335
ENV JIRA_DEFAULT_BOARD_ID=$JIRA_DEFAULT_BOARD_ID
3436
ENV JIRA_DEFAULT_ASSIGNEES=$JIRA_DEFAULT_ASSIGNEES
3537
ENV P_GATEWAY_URL=$P_GATEWAY_URL
38+
ENV OPENAI_ASSISTANT_ID=$OPENAI_ASSISTANT_ID
39+
ENV OPENAI_API_KEY=$OPENAI_API_KEY
3640

3741
RUN yarn build
3842

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const openaiService = require("../services/openaiService");
2+
3+
exports.estimateStoryPoint = async (req, res) => {
4+
try {
5+
const { boardName, issueSummary, issueDescription, threadId } = req.body;
6+
7+
if(!boardName) {
8+
return res.status(400).json({ error: "Board name is required" });
9+
}
10+
11+
if (!issueSummary) {
12+
return res.status(400).json({ error: "Issue Summary is required" });
13+
}
14+
15+
if (!issueDescription) {
16+
return res.status(400).json({ error: "Issue Description is required" });
17+
}
18+
19+
const assistantId = process.env.OPENAI_ASSISTANT_ID;
20+
21+
const message = createStoryPointPrompt(issueSummary, issueDescription);
22+
23+
const response = await openaiService.askAssistant(assistantId, message, threadId);
24+
25+
res.json(response);
26+
} catch (error) {
27+
console.error("Error estimating story point:", error);
28+
res.status(500).json({ error: error.message || "Failed to estimate story point" });
29+
}
30+
};
31+
32+
function createStoryPointPrompt(issueSummary, issueDescription, maxSp = 13) {
33+
return `Lütfen aşağıdaki issue için sadece bir storypoint değeri döndür.
34+
Cevabında kesinlikle başka hiçbir açıklama, metin veya karakter olmasın, sadece rakam olsun.
35+
Sadece sayısal değeri döndür. Örneğin: 5
36+
37+
Bir işe verilebilecek minimum SP değeri 1, maksimum SP değeri ${maxSp}.
38+
Fibonacci serisine göre SP değerleri: 1, 2, 3, 5, 8, 13 ... ... ... olabilir.
39+
40+
Issue Summary: ${issueSummary}
41+
Issue Description: ${issueDescription}`;
42+
}

gurubu-backend/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
"license": "MIT",
66
"scripts": {
77
"build": "webpack --config webpack.config.js",
8-
"start": "nodemon server.js"
8+
"start": "nodemon server.js",
9+
"test": "jest"
910
},
1011
"dependencies": {
1112
"axios": "^1.7.9",
1213
"body-parser": "^1.20.2",
1314
"cors": "^2.8.5",
1415
"dotenv": "^16.3.1",
1516
"express": "^4.18.2",
17+
"openai": "^4.86.2",
1618
"redis": "^4.6.10",
1719
"socket.io": "^4.7.2",
1820
"uuid": "^9.0.1"
@@ -21,7 +23,9 @@
2123
"@babel/core": "^7.23.2",
2224
"@babel/preset-env": "^7.23.2",
2325
"babel-loader": "^9.1.3",
26+
"jest": "^29.7.0",
2427
"nodemon": "^3.0.1",
28+
"supertest": "^7.0.0",
2529
"webpack": "^5.89.0",
2630
"webpack-cli": "^5.1.4",
2731
"webpack-node-externals": "^3.0.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const express = require("express");
2+
const router = express.Router();
3+
const storyPointController = require("../controllers/storyPointEstimationController");
4+
5+
router.post("/estimate", storyPointController.estimateStoryPoint);
6+
7+
module.exports = router;

gurubu-backend/server.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ const roomRoutes = require("./routes/roomRoutes");
1919
const healthCheckRoute = require("./routes/healthCheckRoute");
2020
const jiraRoutes = require("./routes/jiraRoutes");
2121
const pRoutes = require("./routes/pRoutes");
22+
const storyPointRoutes = require("./routes/storyPointRoutes");
2223

2324
app.use("/room", cors(corsOptions), roomRoutes);
2425
app.use("/healthcheck", cors(corsOptions), healthCheckRoute);
2526
app.use("/jira", cors(corsOptions), jiraRoutes);
2627
app.use("/p", cors(corsOptions), pRoutes);
28+
app.use("/storypoint", cors(corsOptions), storyPointRoutes);
2729

28-
const PORT = process.env.PORT || 5000;
30+
const PORT = process.env.PORT || 5001;
2931
const server = app.listen(PORT, () => {
3032
console.log(`Server is running on port ${PORT}`);
3133
});
+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const { OpenAI } = require("openai");
2+
const dotenv = require("dotenv");
3+
4+
dotenv.config();
5+
6+
class OpenAIService {
7+
constructor() {
8+
this.openai = new OpenAI({
9+
apiKey: process.env.OPENAI_API_KEY,
10+
});
11+
}
12+
13+
14+
async askAssistant(assistantId, message, threadId = null) {
15+
try {
16+
17+
let thread;
18+
if (!threadId) {
19+
thread = await this.openai.beta.threads.create();
20+
threadId = thread.id;
21+
} else {
22+
threadId = threadId;
23+
}
24+
25+
await this.openai.beta.threads.messages.create(threadId, {
26+
role: "user",
27+
content: message,
28+
});
29+
30+
const run = await this.openai.beta.threads.runs.create(threadId, {
31+
assistant_id: assistantId,
32+
});
33+
34+
let runStatus = await this.openai.beta.threads.runs.retrieve(
35+
threadId,
36+
run.id
37+
);
38+
39+
while (runStatus.status !== "completed") {
40+
if (runStatus.status === "failed") {
41+
throw new Error(`Run failed with error: ${runStatus.last_error}`);
42+
}
43+
44+
if (["expired", "cancelled"].includes(runStatus.status)) {
45+
throw new Error(`Run ${runStatus.status}`);
46+
}
47+
48+
await new Promise((resolve) => setTimeout(resolve, 1000));
49+
runStatus = await this.openai.beta.threads.runs.retrieve(
50+
threadId,
51+
run.id
52+
);
53+
}
54+
55+
const messages = await this.openai.beta.threads.messages.list(threadId);
56+
57+
const assistantMessages = messages.data.filter(
58+
(msg) => msg.role === "assistant"
59+
);
60+
61+
if (assistantMessages.length === 0) {
62+
return { response: "No response from assistant" };
63+
}
64+
65+
const latestMessage = assistantMessages[0];
66+
67+
const textContent = latestMessage.content
68+
.filter((content) => content.type === "text")
69+
.map((content) => content.text.value)
70+
.join("\n");
71+
72+
return { response: textContent, threadId: threadId };
73+
} catch (error) {
74+
console.error("Error in askAssistant:", error);
75+
throw new Error(`Failed to get response from assistant: ${error.message}`);
76+
}
77+
}
78+
}
79+
80+
module.exports = new OpenAIService();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const request = require('supertest');
2+
const express = require('express');
3+
const app = express();
4+
const bodyParser = require('body-parser');
5+
const cors = require('cors');
6+
7+
require('dotenv').config();
8+
9+
app.use(bodyParser.json());
10+
app.use(cors());
11+
12+
const storyPointRoutes = require('../../routes/storyPointRoutes');
13+
app.use('/storypoint', storyPointRoutes);
14+
15+
describe('Story Point Estimation Controller', () => {
16+
jest.setTimeout(60000); // 60 saniye timeout
17+
18+
// OpenAI API'ye erişim için gerekli ortam değişkenlerini kontrol et
19+
beforeAll(() => {
20+
if (!process.env.OPENAI_API_KEY) {
21+
console.warn('OPENAI_API_KEY ortam değişkeni ayarlanmamış. Test başarısız olabilir.');
22+
}
23+
24+
if (!process.env.OPENAI_ASSISTANT_ID) {
25+
console.warn('OPENAI_ASSISTANT_ID ortam değişkeni ayarlanmamış. Test başarısız olabilir.');
26+
}
27+
});
28+
29+
test('Bir issue için story point tahmini yapabilmeli', async () => {
30+
const testData = {
31+
boardName: 'Test board',
32+
issueSummary: 'Kullanıcı girişi sayfası oluşturulmalı',
33+
issueDescription: 'Kullanıcıların sisteme giriş yapabileceği bir sayfa oluşturulmalı. Sayfa email ve şifre alanı içermeli.'
34+
};
35+
36+
const response = await request(app)
37+
.post('/storypoint/estimate')
38+
.send(testData);
39+
40+
expect(response.status).toBe(200);
41+
expect(response.body).toHaveProperty('response');
42+
expect(response.body).toHaveProperty('threadId');
43+
44+
console.log('OpenAI yanıtı:', response.body.response);
45+
46+
const responseStr = response.body.response.trim();
47+
const isNumeric = /^\d+$/.test(responseStr);
48+
expect(isNumeric).toBe(true);
49+
50+
const validSPValues = [1, 2, 3, 5, 8, 13];
51+
const responseNum = parseInt(responseStr, 10);
52+
expect(validSPValues.includes(responseNum)).toBe(true);
53+
54+
// Thread ID'yi sakla
55+
const threadId = response.body.threadId;
56+
console.log('Thread ID:', threadId);
57+
58+
59+
if (threadId) {
60+
const followUpData = {
61+
boardName: 'Test board',
62+
issueSummary: 'Kullanıcı girişi sayfası güncellenmeli',
63+
issueDescription: 'Mevcut giriş sayfasına şifre sıfırlama özelliği eklenmeli.',
64+
threadId: threadId
65+
};
66+
67+
const followUpResponse = await request(app)
68+
.post('/storypoint/estimate')
69+
.send(followUpData);
70+
71+
expect(followUpResponse.status).toBe(200);
72+
expect(followUpResponse.body).toHaveProperty('response');
73+
expect(followUpResponse.body).toHaveProperty('threadId');
74+
expect(followUpResponse.body.threadId).toBe(threadId);
75+
76+
console.log('Takip sorusu yanıtı:', followUpResponse.body.response);
77+
78+
const followUpResponseStr = followUpResponse.body.response.trim();
79+
const isFollowUpNumeric = /^\d+$/.test(followUpResponseStr);
80+
expect(isFollowUpNumeric).toBe(true);
81+
82+
const validSPValues = [1, 2, 3, 5, 8, 13];
83+
const followUpResponseNum = parseInt(followUpResponseStr, 10);
84+
expect(validSPValues.includes(followUpResponseNum)).toBe(true);
85+
}
86+
});
87+
88+
});

0 commit comments

Comments
 (0)