From aca7037a82cddd18d94845ac07ebbb7acad559db Mon Sep 17 00:00:00 2001 From: aman-pandey-afk Date: Wed, 6 Nov 2024 14:30:00 +0530 Subject: [PATCH 01/62] Revamp UI and responsive design --- server/requirements.txt | Bin 236 -> 282 bytes src/static/css/auth/login.css | 124 +++++++++++++++++-- src/static/css/base/_variables.css | 41 ++++-- src/static/css/components/_button.css | 33 +++++ src/static/css/components/_cards.css | 24 ++++ src/static/css/components/_forms.css | 56 ++++++++- src/static/css/components/_header.css | 25 ---- src/static/css/components/_layout.css | 53 ++++++++ src/static/css/components/_panel.css | 5 +- src/static/css/components/_sidenav.css | 113 +++++++++++++++++ src/static/css/main.css | 4 +- src/static/images/default-avatar.png | Bin 0 -> 2411 bytes src/static/images/google-icon.png | Bin 0 -> 32085 bytes src/static/images/logo.png | Bin 0 -> 6766 bytes src/static/js/components/characterManager.js | 4 +- src/static/js/components/datasetManager.js | 4 +- src/static/js/components/header.js | 25 ---- src/static/js/components/navigation.js | 95 ++++++++++++++ src/static/js/main.js | 4 +- src/templates/components/main-content.html | 51 ++++++++ src/templates/components/sidenav.html | 28 +++++ src/templates/components/sliding-panel.html | 8 ++ src/templates/index.html | 70 ++--------- src/templates/login.html | 12 +- 24 files changed, 629 insertions(+), 150 deletions(-) create mode 100644 src/static/css/components/_button.css delete mode 100644 src/static/css/components/_header.css create mode 100644 src/static/css/components/_layout.css create mode 100644 src/static/css/components/_sidenav.css create mode 100644 src/static/images/default-avatar.png create mode 100644 src/static/images/google-icon.png create mode 100644 src/static/images/logo.png delete mode 100644 src/static/js/components/header.js create mode 100644 src/static/js/components/navigation.js create mode 100644 src/templates/components/main-content.html create mode 100644 src/templates/components/sidenav.html create mode 100644 src/templates/components/sliding-panel.html diff --git a/server/requirements.txt b/server/requirements.txt index b469483e5563bca205828ede6135e9a298d9fec9..1cf0439e910a296b4f3abaf52050d096b2a71725 100644 GIT binary patch delta 51 zcmaFEIE!h5hGrT=CPNWJDnk-OB118d)&B8A01}V`?EnA( diff --git a/src/static/css/auth/login.css b/src/static/css/auth/login.css index 1c9b7ad..50d3ce2 100644 --- a/src/static/css/auth/login.css +++ b/src/static/css/auth/login.css @@ -1,30 +1,128 @@ -.login-container { +:root { + --primary-color: #6C63FF; + --secondary-color: #2A2A2A; + --background-color: #F5F7FF; + --text-color: #333333; + --shadow-color: rgba(108, 99, 255, 0.2); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + background: var(--background-color); + min-height: 100vh; display: flex; - justify-content: center; align-items: center; - min-height: 100vh; - background-color: #f5f5f5; + justify-content: center; + position: relative; + overflow: hidden; +} + +.background-shapes { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; +} + +.background-shapes::before, +.background-shapes::after { + content: ''; + position: absolute; + width: 300px; + height: 300px; + border-radius: 50%; + background: linear-gradient(45deg, var(--primary-color), #8B85FF); + opacity: 0.1; +} + +.background-shapes::before { + top: -100px; + right: -100px; +} + +.background-shapes::after { + bottom: -100px; + left: -100px; +} + +.login-container { + width: 100%; + max-width: 400px; + padding: 20px; + z-index: 1; } .login-box { background: white; - padding: 2rem; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 40px; + border-radius: 20px; + box-shadow: 0 10px 30px var(--shadow-color); text-align: center; + transform: translateY(0); + transition: transform 0.3s ease; +} + +.login-box:hover { + transform: translateY(-5px); +} + +.logo { + margin-bottom: 20px; +} + +.logo img { + width: 80px; + height: auto; +} + +h1 { + color: var(--secondary-color); + font-size: 28px; + margin-bottom: 10px; +} + +.subtitle { + color: #666; + font-size: 16px; + margin-bottom: 30px; } .google-btn { - background: #4285f4; - color: white; + background: white; + color: var(--text-color); padding: 12px 24px; - border: none; - border-radius: 4px; + border: 2px solid #eee; + border-radius: 50px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; font-size: 16px; - margin: 20px 0; + font-weight: 500; + width: 100%; + transition: all 0.3s ease; } .google-btn:hover { - background: #357abd; + background: #f8f8f8; + border-color: #ddd; + transform: translateY(-2px); +} + +.google-btn img { + width: 20px; + height: 20px; +} + +@media (max-width: 480px) { + .login-box { + padding: 30px 20px; + } } \ No newline at end of file diff --git a/src/static/css/base/_variables.css b/src/static/css/base/_variables.css index fd0902e..d81b0ec 100644 --- a/src/static/css/base/_variables.css +++ b/src/static/css/base/_variables.css @@ -1,26 +1,43 @@ :root { /* Colors */ - --primary-color: #3498db; - --secondary-color: #2c3e50; - --accent-color: #e74c3c; - --background-color: #ecf0f1; - --text-color: #34495e; + --primary-color: #6C63FF; + --secondary-color: #2A2A2A; + --accent-color: #8B85FF; + --background-color: #F5F7FF; + --text-color: #333333; --card-background: #ffffff; - --shadow-color: rgba(0, 0, 0, 0.1); + --shadow-color: rgba(108, 99, 255, 0.2); + --hover-color: #F0F2FF; + + /* Layout-specific */ + --sidenav-width: 280px; + --header-height: 60px; + --panel-width: 400px; /* Typography */ - --heading-font: 'Alfa Slab One', cursive; - --body-font: 'IBM Plex Mono', monospace; + --heading-font: 'Poppins', sans-serif; + --body-font: 'Poppins', sans-serif; /* Spacing */ - --spacing-small: 10px; - --spacing-medium: 20px; - --spacing-large: 40px; + --spacing-small: 12px; + --spacing-medium: 24px; + --spacing-large: 40px; /* Transitions */ - --transition-speed: 0.3s; + --transition-speed: 0.3s; + --transition-timing: ease; /* Shadows */ --shadow-default: 0 4px 6px var(--shadow-color); --shadow-hover: 0 6px 8px var(--shadow-color); + --shadow-sidenav: 2px 0 10px rgba(0, 0, 0, 0.1); + + /* Border Radius */ + --border-radius-small: 8px; + --border-radius-medium: 12px; + --border-radius-large: 20px; + + /* Border Colors */ + --border-color: #eee; + --border-color-hover: #ddd; } \ No newline at end of file diff --git a/src/static/css/components/_button.css b/src/static/css/components/_button.css new file mode 100644 index 0000000..dea67d9 --- /dev/null +++ b/src/static/css/components/_button.css @@ -0,0 +1,33 @@ +.button { + padding: 12px 24px; + border-radius: 8px; + border: none; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 500; + font-family: var(--body-font); + font-size: 14px; + line-height: 1; + transition: all var(--transition-speed) var(--transition-timing); +} + +.button .material-icons { + font-size: 20px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.button--primary { + background: var(--primary-color); + color: white; +} + +.button--primary:hover { + background: var(--accent-color); + transform: translateY(-2px); + box-shadow: var(--shadow-default); +} \ No newline at end of file diff --git a/src/static/css/components/_cards.css b/src/static/css/components/_cards.css index 749cd89..c28dfb2 100644 --- a/src/static/css/components/_cards.css +++ b/src/static/css/components/_cards.css @@ -43,6 +43,16 @@ border-radius: 4px; cursor: pointer; transition: background-color var(--transition-speed) ease; + display: inline-flex; + align-items: center; + gap: 4px; + line-height: 1; +} + +.card__button--delete .material-icons { + font-size: 16px; + display: inline-flex; + align-items: center; } .card__button--delete:hover { @@ -53,4 +63,18 @@ display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: var(--spacing-medium); +} + +@media screen and (max-width: 768px) { + .card-grid { + display: grid; + grid-template-columns: 280px; + gap: var(--spacing-medium); + padding-left: 20px; + } + + .card { + width: 100%; + margin: 0; + } } \ No newline at end of file diff --git a/src/static/css/components/_forms.css b/src/static/css/components/_forms.css index 8d4191b..bd61163 100644 --- a/src/static/css/components/_forms.css +++ b/src/static/css/components/_forms.css @@ -22,19 +22,69 @@ } .form__range { + -webkit-appearance: none; + appearance: none; width: calc(100% - 60px); margin-right: 10px; vertical-align: middle; + height: 4px; + background: var(--primary-color); + border-radius: 2px; +} + +/* Track styling */ +.form__range::-webkit-slider-runnable-track { + height: 4px; + border-radius: 2px; +} + +.form__range::-moz-range-track { + height: 4px; + border-radius: 2px; +} + +/* Thumb styling */ +.form__range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + margin-top: -6px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + transition: background-color var(--transition-speed) ease; +} + +.form__range::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--primary-color); + border: none; + border-radius: 50%; + cursor: pointer; + transition: background-color var(--transition-speed) ease; +} + +/* Hover states */ +.form__range::-webkit-slider-thumb:hover { + background: var(--accent-color); +} + +.form__range::-moz-range-thumb:hover { + background: var(--accent-color); } .form__range-value { display: inline-block; width: 50px; text-align: center; - background-color: #f0f0f0; + background: var(--card-background); padding: 5px; border-radius: 4px; font-size: 0.9em; + border: 1px solid var(--border-color); + color: var(--text-color); } /* Buttons */ @@ -53,7 +103,7 @@ } .button--primary:hover { - background-color: #2980b9; + background-color: var(--accent-color); transform: translateY(-2px); } @@ -85,7 +135,7 @@ select.form__input { background-repeat: no-repeat; background-position: right 8px center; padding-right: 30px; - height: 38px; + height: 42px; } .evaluation-result { diff --git a/src/static/css/components/_header.css b/src/static/css/components/_header.css deleted file mode 100644 index 3b24b5a..0000000 --- a/src/static/css/components/_header.css +++ /dev/null @@ -1,25 +0,0 @@ -.header-content { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; -} - -.user-info { - display: flex; - align-items: center; -} - -button { - background-color: #4285f4; - color: white; - border: none; - padding: 5px 10px; - border-radius: 4px; - cursor: pointer; - margin-left: 10px; -} - -button:hover { - background-color: #357abd; -} \ No newline at end of file diff --git a/src/static/css/components/_layout.css b/src/static/css/components/_layout.css new file mode 100644 index 0000000..99037f0 --- /dev/null +++ b/src/static/css/components/_layout.css @@ -0,0 +1,53 @@ +.app-container { + display: flex; + min-height: 100vh; + font-family: var(--body-font); +} + +.main-content { + margin-left: var(--sidenav-width); + padding: var(--spacing-large); + width: calc(100% - var(--sidenav-width)); + background: var(--background-color); +} + +.content-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: var(--spacing-large); +} + +.content-header h1 { + color: var(--secondary-color); + font-family: var(--heading-font); + font-size: 24px; +} + +@media screen and (max-width: 768px) { + .main-content { + margin-left: 0; + padding: 20px; + padding-left: 60px; + width: 100%; + box-sizing: border-box; + } + + .content-header { + padding: 0 16px; + margin-top: 10px; + } + + .content-header h1 { + margin-left: 10px; + } +} + +.tab-content { + display: none; + width: 100%; +} + +.tab-content.active { + display: block; +} \ No newline at end of file diff --git a/src/static/css/components/_panel.css b/src/static/css/components/_panel.css index da7b894..917a74d 100644 --- a/src/static/css/components/_panel.css +++ b/src/static/css/components/_panel.css @@ -3,12 +3,13 @@ position: fixed; top: 0; right: -100%; - width: 70%; + width: var(--panel-width, 400px); height: 100%; background-color: var(--card-background); box-shadow: -2px 0 5px var(--shadow-color); z-index: 1000; - overflow: scroll; + overflow-y: auto; + overflow-x: hidden; padding: 40px; visibility: hidden; } diff --git a/src/static/css/components/_sidenav.css b/src/static/css/components/_sidenav.css new file mode 100644 index 0000000..7346ea4 --- /dev/null +++ b/src/static/css/components/_sidenav.css @@ -0,0 +1,113 @@ +.sidenav { + width: var(--sidenav-width); + background: var(--card-background); + box-shadow: var(--shadow-sidenav); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; +} + +.sidenav__user { + padding: 24px; + display: flex; + align-items: center; + gap: 12px; + border-bottom: 1px solid var(--border-color); +} + +.sidenav__user img { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; +} + +.sidenav__user span { + font-weight: 500; + color: var(--secondary-color); +} + +.sidenav__menu { + flex-grow: 1; + padding: var(--spacing-medium) 0; +} + +.sidenav__item { + display: flex; + align-items: center; + padding: var(--spacing-small) var(--spacing-medium); + color: var(--text-color); + text-decoration: none; + transition: all var(--transition-speed) var(--transition-timing); + gap: var(--spacing-small); +} + +.sidenav__item:hover { + background: var(--hover-color); +} + +.sidenav__item.active { + background: var(--hover-color); + color: var(--primary-color); + border-left: 4px solid var(--primary-color); +} + +.sidenav__footer { + padding: var(--spacing-medium); + border-top: 1px solid var(--border-color); +} + +.sidenav__signout { + width: 100%; + padding: var(--spacing-small); + background: none; + border: 2px solid var(--border-color); + border-radius: var(--border-radius-small); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-small); + transition: all var(--transition-speed) var(--transition-timing); + color: var(--text-color); +} + +.sidenav__signout:hover { + background: var(--hover-color); + border-color: var(--border-color-hover); +} + +/* Mobile styles */ +@media screen and (max-width: 768px) { + .sidenav { + position: fixed; + left: -100%; + z-index: 1000; + transition: left 0.3s ease; + } + + .sidenav.active { + left: 0; + } + + /* Add hamburger menu button */ + .menu-toggle { + display: block; + position: absolute; + top: var(--spacing-medium); + left: var(--spacing-medium); + z-index: 999; + background: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + padding: var(--spacing-small); + cursor: pointer; + margin-top: 6px; + } + + .menu-toggle .material-icons { + font-size: 18px; + } +} diff --git a/src/static/css/main.css b/src/static/css/main.css index ef4b024..7f7faa2 100644 --- a/src/static/css/main.css +++ b/src/static/css/main.css @@ -3,4 +3,6 @@ @import 'components/_cards.css'; @import 'components/_forms.css'; @import 'components/_panel.css'; -@import 'components/_header.css'; \ No newline at end of file +@import 'components/_layout.css'; +@import 'components/_sidenav.css'; +@import 'components/_button.css' \ No newline at end of file diff --git a/src/static/images/default-avatar.png b/src/static/images/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..2b587a66ab3b95685864bd069427cb9e53ed7381 GIT binary patch literal 2411 zcmd^9Yc!Po7XLqUVaT|PEV)E3m1~;9V7wX1t*KX0YAB`h3N=LrJ&!IcnxvCTGBK%8 zk$FWC!=zIvm*$|nZljaSl-tNK#LRQ1v(A_E@q9k}%ih1W_xkS z0EpYSdG7)MqX;p8fK|9RMHbBphh_Y>J(xfs^nKYeq=0I)fM6=;4Z8L85IJ5LcV{wv z>&0yVsI1uT?G=2MJvq>q(6>b!Ph%~k@U=56`#%q5;6kmt24`>QcR#)0N(MH$=aPTX z|Bp7xtuG6&L!nJ|JC-E;wJHJQi0eBQq2s$;w8c&{qGZvZKT~KWKOkWS>$p$D)bee%_d8&d zbb7X*k{j6XqGV(_ykydljXbz|R#reQ_t!Hw2JI>ym5VTux@g||sM{i6J&K6;_+G^5F*1#81F+szp0v^?0$J>Bhr*1mh$Y+YNE&tb(lVnAiKL<^2{nRw5=kF&5+M z5gNxV;sBW#yXH$GB)>whwK=?yEA}n?9IGnW+HcOBjJlqr98WlA#!OH4E+0_S7UHTk zV$2vI1|l9-N0g44DiQ<=_VF}^jgfv>HrfUZ(~ZA)2%c_E>%yCYC2Cf@zL3ykD_unc z^B?N)3*R_OPRbPQM9!iYN9mY?$(e%9#d09SW+OoShydLjSCEgJ(syq>;LJZK*w}By zoU_z)Z%i!=iNiehR1;@%WKN!{l3QFQ#hk4W7wq&RFpo@55PM~D>3`@l8KYyVzC#T1l)_~1CNMB8JB2_O9=7B8>RUmhRs$4-DY z0_BVgR+AU4laW6{5{=ceya&{MP+kpd8~>0TtFnbqg5ED}x~%|jT*Moyx5zR@T0Kz2axa@s|;XL;lCRNeTw;@Lq=TXe+9 zx#F%ED+gKW{AYyaS?j*aLFQ6gY@7VL@TXNp{T>|acl6??x!&&$Rq~}+4gsDa8Vq__~m&R6!raB=JRXbDAzLEUp}6F3AM1Ok1lY4=m9y%nk#d;oG`} zDWTjhPXG4#NVQWVFvHEz$G&~y)13J0;CdsqTEk}xaC}mVHFmXyfjpcgBnd)`z6lra z>6DdTmK|CsGE&W}X8&|*M;oW}|FVgkgTHR!{)Ovom>C}Dpq-ZLKdFny}+tzf~_zbrGjjitAu6FyKV&%1}00ADVX}uqe&uZ zr8KWmL^bg(n{U`lHm$dDk>`o3OuVYv(2Yx!Zy(UopB}Wu9}Vl-$oCwyd)K@LZLCFT zejMKt7<^j|NFY5^3O{+72ju(~nYPg?5x=gGAVK9YZsIIe7z<l zIEogIb>@wun;D77B5KFuIZ_`6&B86GO^CgKz9|F~1Ik(Xd;Rl4UF?%UQ>G*1O8!1K5@Kul1k4H# z-wpG^b6cF+EH!yIlBheL5%Jt^i(*7{q;&%`!PXj?EGj`n)K*(2ljv=KFj}Wyr5BpN z6QPk#Z|t`uAebI=w1d?PCA_CY9)v-gWT49r!qC_tSl|71DjP~)NJQE_&{YII(Ls8hDQ7UWJf)H#5Zwv7RVXJ6Ij;Zb e#bMJ*3s?tfP*K|jW$Lf%-A)bie)e0$`F{bZXG5p} literal 0 HcmV?d00001 diff --git a/src/static/images/google-icon.png b/src/static/images/google-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..35dc31de7fd25f7005f32167f7404e8c6c40fa19 GIT binary patch literal 32085 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/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.name}

${data.backstory}

- `; diff --git a/src/static/js/components/datasetManager.js b/src/static/js/components/datasetManager.js index 8fb2463..58e9076 100644 --- a/src/static/js/components/datasetManager.js +++ b/src/static/js/components/datasetManager.js @@ -40,8 +40,8 @@ export class DatasetManager {

${data.name}

Metric: ${data.metric_name}

Questions: ${data.questions.length}

- `; diff --git a/src/static/js/components/header.js b/src/static/js/components/header.js deleted file mode 100644 index 8e79a0b..0000000 --- a/src/static/js/components/header.js +++ /dev/null @@ -1,25 +0,0 @@ -import { logout } from "../api/auth.js"; -import { getCurrentSession } from "../api/session.js"; - -export async function initializeHeader() { - // Fetch user session info - const response = await getCurrentSession(); - if (response.ok) { - const data = await response.json(); - const user = data.user; - document.getElementById('userName').textContent = `Hello, ${user.name}`; - } else { - // Redirect to login if session is not valid - window.location.href = '/login'; - } - - // Logout functionality - document.getElementById('logoutButton').addEventListener('click', async () => { - const response = await logout(); - if (response.ok) { - window.location.href = '/login'; - } else { - console.error('Logout failed'); - } - }); -} \ No newline at end of file diff --git a/src/static/js/components/navigation.js b/src/static/js/components/navigation.js new file mode 100644 index 0000000..8fb4469 --- /dev/null +++ b/src/static/js/components/navigation.js @@ -0,0 +1,95 @@ +import { logout } from "../api/auth.js"; +import { getCurrentSession } from "../api/session.js"; + +export class Navigation { + constructor() { + this.tabItems = document.querySelectorAll('.sidenav__item'); + this.tabContents = document.querySelectorAll('.tab-content'); + this.userNameElement = document.getElementById('userName'); + this.userAvatarElement = document.getElementById('userAvatar'); + this.signOutBtn = document.getElementById('signOutBtn'); + this.menuToggle = document.getElementById('menuToggle'); + this.sidenav = document.querySelector('.sidenav'); + + this.initializeNavigation(); + } + + async initializeNavigation() { + await this.loadUserInfo(); + this.initializeEventListeners(); + } + + async loadUserInfo() { + try { + const response = await getCurrentSession(); + if (response.ok) { + const data = await response.json(); + const user = data.user; + this.updateUserInfo(user); + } else { + // Redirect to login if session is not valid + window.location.href = '/login'; + } + } catch (error) { + console.error('Failed to load user info:', error); + window.location.href = '/login'; + } + } + + initializeEventListeners() { + // Tab navigation + this.tabItems.forEach(item => { + item.addEventListener('click', (e) => this.handleTabClick(e)); + }); + + // Sign out functionality + this.signOutBtn.addEventListener('click', async () => { + try { + const response = await logout(); + if (response.ok) { + window.location.href = '/login'; + } else { + console.error('Logout failed'); + } + } catch (error) { + console.error('Logout error:', error); + } + }); + + // Mobile menu toggle + if (this.menuToggle) { + this.menuToggle.addEventListener('click', () => { + this.sidenav.classList.toggle('active'); + }); + + // Close menu when clicking outside + document.addEventListener('click', (e) => { + if (!this.sidenav.contains(e.target) && + !this.menuToggle.contains(e.target) && + this.sidenav.classList.contains('active')) { + this.sidenav.classList.remove('active'); + } + }); + } + } + + handleTabClick(e) { + e.preventDefault(); + + this.tabItems.forEach(tab => tab.classList.remove('active')); + this.tabContents.forEach(content => content.classList.remove('active')); + + const clickedTab = e.currentTarget; + clickedTab.classList.add('active'); + + const tabId = `${clickedTab.dataset.tab}Tab`; + document.getElementById(tabId).classList.add('active'); + } + + updateUserInfo(user) { + this.userNameElement.textContent = user.name; + if (user.picture) { + this.userAvatarElement.src = user.picture; + } + } +} \ No newline at end of file diff --git a/src/static/js/main.js b/src/static/js/main.js index baeffc2..5a8cf68 100644 --- a/src/static/js/main.js +++ b/src/static/js/main.js @@ -1,10 +1,10 @@ import { CharacterManager } from './components/characterManager.js'; import { DatasetManager } from './components/datasetManager.js'; import { EvaluationManager } from './components/evaluationManager.js'; -import { initializeHeader } from './components/header.js'; +import { Navigation } from './components/navigation.js' document.addEventListener('DOMContentLoaded', () => { - initializeHeader(); + new Navigation(); new CharacterManager(); new DatasetManager(); new EvaluationManager(); diff --git a/src/templates/components/main-content.html b/src/templates/components/main-content.html new file mode 100644 index 0000000..a0e1b12 --- /dev/null +++ b/src/templates/components/main-content.html @@ -0,0 +1,51 @@ + + +
+ +
+
+

Characters

+ +
+
+ +
+
+ + +
+
+

Datasets

+ +
+
+ +
+
+ + +
+
+

Evaluation

+
+
+ + + + +
+
+
+
\ No newline at end of file diff --git a/src/templates/components/sidenav.html b/src/templates/components/sidenav.html new file mode 100644 index 0000000..215f159 --- /dev/null +++ b/src/templates/components/sidenav.html @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/src/templates/components/sliding-panel.html b/src/templates/components/sliding-panel.html new file mode 100644 index 0000000..ffe2e65 --- /dev/null +++ b/src/templates/components/sliding-panel.html @@ -0,0 +1,8 @@ +
+
+ +

+
+
\ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index 49c8e2c..fb0408f 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -3,71 +3,21 @@ - Character and Dataset Manager - + ProBack + + + + + -
-
-
-

Character and Dataset Manager

- -
-
- -
-
-
-

Characters

- -
-
-
- -
-
-

Datasets

- -
-
-
- -
-
-

Evaluation

-
-
- - - - -
-
-
-
-
- -
- -

-
+
+ {% include 'components/sidenav.html' %} + {% include 'components/main-content.html' %}
-
+ {% include 'components/sliding-panel.html' %} diff --git a/src/templates/login.html b/src/templates/login.html index f67eccf..8d147e3 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -3,15 +3,21 @@ - Login - Character Manager + Login - ProBack + +
- {% include 'components/sliding-panel.html' %} - \ No newline at end of file From 2db94c424f8de0a2c1aefd55931b5f0aacf48911 Mon Sep 17 00:00:00 2001 From: Aman Pandey <77021852+Aman-Pandey-afk@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:38:53 +0530 Subject: [PATCH 06/62] Update Dataset Handling * AI Agent Save and Load * Playground overhaul * AI Agent Restructure and Code Refactor * Dataset Operations and UI --- server/app/api/dataset_routes.py | 78 ++- server/app/core/datasets.py | 122 ++--- server/app/core/models.py | 7 +- server/app/firebase/dataset_dao.py | 12 +- src/static/css/components/_dataset.css | 162 ++++-- src/static/css/components/_forms.css | 1 + src/static/js/api/datasets.js | 26 + .../js/components/dataset/datasetEditor.js | 507 +++++++++++------- .../js/components/dataset/datasetManager.js | 72 ++- .../js/components/navigation/navigation.js | 22 +- 10 files changed, 623 insertions(+), 386 deletions(-) diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index b6ac093..34751b0 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -1,12 +1,18 @@ from flask import Blueprint, request, jsonify, session -from ..core.datasets import ( - get_all_datasets, save_dataset, delete_dataset, - get_dataset, update_dataset, SubjectiveDataset, ObjectiveDataset -) +from ..core.datasets import Dataset, get_all_datasets, get_dataset from ..auth.decorators import login_required dataset_bp = Blueprint('datasets', __name__) +@dataset_bp.route('/datasets/', methods=['GET']) +@login_required +def get_dataset_by_id(dataset_id): + user_id = session['user']['uid'] + dataset = get_dataset(dataset_id, user_id) + if dataset: + return jsonify(dataset) + return jsonify({"message": "Dataset not found"}), 404 + @dataset_bp.route('/datasets', methods=['GET']) @login_required def get_datasets(): @@ -18,36 +24,59 @@ def get_datasets(): def create_dataset(): data = request.json user_id = session['user']['uid'] - dataset_id = save_dataset( - name=data['name'], - metric_name=data['metric_name'], - metric_description=data['metric_description'], - questions=data['questions'], - user_id=user_id - ) - return jsonify({"message": f"Dataset '{data['name']}' created successfully", "id": dataset_id}), 201 + + try: + # Create dataset instance + dataset = Dataset(name=data['name']) + + # Add samples + for sample in data['samples']: + dataset.add_sample(sample) + + # Save dataset + dataset_id = dataset.save(user_id) + + return jsonify({ + "message": f"Dataset '{data['name']}' created successfully", + "id": dataset_id + }), 201 + + except ValueError as e: + return jsonify({"message": str(e)}), 400 + except Exception as e: + print(f"Error creating dataset: {str(e)}") + return jsonify({"message": "Failed to create dataset"}), 500 @dataset_bp.route('/datasets/', methods=['PUT']) @login_required def update_dataset_route(dataset_id): data = request.json user_id = session['user']['uid'] - if update_dataset( - dataset_id=dataset_id, - name=data['name'], - metric_name=data['metric_name'], - metric_description=data['metric_description'], - questions=data['questions'], - user_id=user_id - ): - return jsonify({"message": "Dataset updated successfully"}) - return jsonify({"message": "Dataset not found"}), 404 + + try: + # Create dataset instance + dataset = Dataset(name=data['name']) + + # Add samples + for sample in data['samples']: + dataset.add_sample(sample) + + # Update dataset + if dataset.update(dataset_id, user_id): + return jsonify({"message": "Dataset updated successfully"}) + return jsonify({"message": "Dataset not found"}), 404 + + except ValueError as e: + return jsonify({"message": str(e)}), 400 + except Exception as e: + print(f"Error updating dataset: {str(e)}") + return jsonify({"message": "Failed to update dataset"}), 500 @dataset_bp.route('/datasets/', methods=['DELETE']) @login_required def remove_dataset(dataset_id): user_id = session['user']['uid'] - if delete_dataset(dataset_id, user_id): + if Dataset.delete(dataset_id, user_id): return jsonify({"message": "Dataset deleted successfully"}) return jsonify({"message": "Dataset not found"}), 404 @@ -56,7 +85,8 @@ def remove_dataset(dataset_id): def generate_questions(): try: data = request.json - dataset_class = SubjectiveDataset if data['datasetType'] == 'subjective' else ObjectiveDataset + + dataset_class = Dataset dataset = dataset_class(name="temp", id="temp") questions = dataset.generate_questions( diff --git a/server/app/core/datasets.py b/server/app/core/datasets.py index 56245ef..5752d32 100644 --- a/server/app/core/datasets.py +++ b/server/app/core/datasets.py @@ -8,29 +8,49 @@ def get_all_datasets(user_id: str) -> Dict: """Get all datasets for a user""" return dataset_dao.get_all(user_id) -def save_dataset(name: str, metric_name: str, metric_description: str, questions: List[str], user_id: str) -> str: - """Create new dataset""" - return dataset_dao.save(name, metric_name, metric_description, questions, user_id) - -def delete_dataset(dataset_id: str, user_id: str) -> bool: - """Delete a dataset""" - return dataset_dao.delete(dataset_id, user_id) - def get_dataset(dataset_id: str, user_id: str) -> Dict: """Get a specific dataset""" return dataset_dao.get_by_id(dataset_id, user_id) -def update_dataset(dataset_id: str, name: str, metric_name: str, metric_description: str, questions: List[str], user_id: str) -> bool: - """Update existing dataset""" - return dataset_dao.update(dataset_id, name, metric_name, metric_description, questions, user_id) - class Dataset: - def __init__(self, name: str, id: str) -> None: + def __init__(self, name: str, id: str = None) -> None: self.name = name - self.id = id + self.id = id + self.samples = [] # List of dictionaries + + def add_sample(self, sample: Dict) -> None: + """Add a sample to the dataset""" + if not (sample.get('free_variables') or sample.get('user_prompt')): + raise ValueError("Sample must contain either free variables or user prompt") + self.samples.append(sample) + + def validate_sample(self, sample: Dict) -> bool: + """Validate if a sample has required fields""" + return bool(sample.get('free_variables') or sample.get('user_prompt')) + + def save(self, user_id: str) -> str: + """Save the dataset to database""" + return dataset_dao.save( + name=self.name, + samples=self.samples, + user_id=user_id + ) + + def update(self, dataset_id: str, user_id: str) -> bool: + """Update the dataset in database""" + return dataset_dao.update( + dataset_id=dataset_id, + name=self.name, + samples=self.samples, + user_id=user_id + ) + + @staticmethod + def delete(dataset_id: str, user_id: str) -> bool: + """Delete a dataset""" + return dataset_dao.delete(dataset_id, user_id) def load(self, inputs): - self.inputs = inputs def format_prompt(self, prompt: str, inputs: dict) -> str: @@ -66,17 +86,12 @@ def generate_samples(self) -> list: raise NotImplementedError("Subclasses must implement generate_samples") def generate_input_testcases(self,model,prompt_template,num_test_cases,input_descriptions) -> list: - input_desc = "" json_example = "" for _input in input_descriptions: - if input_descriptions[_input] is not None: - input_desc += f"{_input}: {input_descriptions[_input]}, " - else: - input_desc += f"{_input}," json_example += f'"{_input}": ........,' @@ -102,70 +117,3 @@ def generate_input_testcases(self,model,prompt_template,num_test_cases,input_des except Exception as e: print(e) print("The LLM is probably not returning a valid JSON object, retry it probably") - -class SubjectiveDataset(Dataset): - - def __init__(self, name: str, id: str) -> None: - super().__init__(name, id) - self.eval_scheme = None - - def generate_samples(self,model: str,agent_prompt: str ,user_request: str, num_q: str) -> list: - - - service = LLMResponseService( - model=model, - system_prompt="", - persist_history=False - ) - - message = subjective_input_sample_generation_template.format( - agent_prompt = agent_prompt, - user_request = user_request, - num_q = num_q - ) - - response = service.send_message(message) - - self.eval_scheme = user_request - - try: - questions_list = eval(response) - - return questions_list - except Exception as e: - print(e) - print("the LLM is probably not returning a valid list object, need to retry") - print("The LLM response was: ", response) - -class ObjectiveDataset(Dataset): - - def __init__(self, name: str, id: str) -> None: - super().__init__(name, id) - - def generate_samples(self,model: str,agent_prompt: str ,user_request: str, num_q: str) -> list: - - - service = LLMResponseService( - model=model, - system_prompt="", - persist_history=False - ) - - message = objective_input_output_sample_generation_template.format( - agent_prompt = agent_prompt, - user_request = user_request, - num_q = num_q - ) - - response = service.send_message(message) - - self.eval_scheme = user_request - - try: - questions_list = eval(response) - - return questions_list - except Exception as e: - print(response) - print(e) - print("the LLM is probably not returning a valid list object, need to retry") diff --git a/server/app/core/models.py b/server/app/core/models.py index ab366db..3048519 100644 --- a/server/app/core/models.py +++ b/server/app/core/models.py @@ -1,5 +1,5 @@ from ..utils import system_score -from .datasets import Dataset, SubjectiveDataset, ObjectiveDataset +from .datasets import Dataset from ..utils.api_clients import LLMResponseService from ..utils.prompts import system_score from typing import Dict, Optional @@ -118,11 +118,6 @@ def auto_score(self, eval_scheme: str, score_guide: str, config: dict): ''' def match_output_score(self): - - if not isinstance(self.dataset, ObjectiveDataset): - - raise TypeError("The dataset is not of type ObjectiveDataset.") - ''' Logic to iterate through the dataset and then send the inputs to 'agent' and then match the agent response to dataset output. ''' diff --git a/server/app/firebase/dataset_dao.py b/server/app/firebase/dataset_dao.py index 67c1701..043a805 100644 --- a/server/app/firebase/dataset_dao.py +++ b/server/app/firebase/dataset_dao.py @@ -18,22 +18,20 @@ def get_by_id(dataset_id: str, user_id: str) -> Optional[Dict]: return doc.to_dict() return None -def save(name: str, metric_name: str, metric_description: str, questions: List[str], user_id: str) -> str: +def save(name: str, samples: List[Dict], user_id: str) -> str: """Save new dataset with user_id""" doc_ref = db.collection('datasets').document() dataset_data = { "name": name, - "metric_name": metric_name, - "metric_description": metric_description, - "questions": questions, + "samples": samples, "user_id": user_id } doc_ref.set(dataset_data) return doc_ref.id -def update(dataset_id: str, name: str, metric_name: str, metric_description: str, questions: List[str], user_id: str) -> bool: +def update(dataset_id: str, name: str, samples: List[Dict], user_id: str) -> bool: """Update existing dataset if it belongs to the user""" dataset_ref = db.collection('datasets').document(dataset_id) doc = dataset_ref.get() @@ -41,9 +39,7 @@ def update(dataset_id: str, name: str, metric_name: str, metric_description: str if doc.exists and doc.to_dict().get('user_id') == user_id: dataset_ref.update({ "name": name, - "metric_name": metric_name, - "metric_description": metric_description, - "questions": questions + "samples": samples }) return True return False diff --git a/src/static/css/components/_dataset.css b/src/static/css/components/_dataset.css index d9f151d..c11b397 100644 --- a/src/static/css/components/_dataset.css +++ b/src/static/css/components/_dataset.css @@ -1,5 +1,6 @@ +/* Editor Layout */ .dataset-editor { - padding: 2rem; + padding: var(--spacing-medium); max-width: 1200px; margin: 0 auto; } @@ -8,21 +9,22 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 2rem; + margin-bottom: var(--spacing-medium); } .editor-row { display: flex; - gap: 2rem; - margin-bottom: 2rem; + gap: var(--spacing-medium); + margin-bottom: var(--spacing-medium); } -.dataset-type, +/* Form Elements */ +.dataset-name, .agent-select { flex: 1; } -.dataset-type select, +/* Using form styles for select */ .agent-select select { width: 100%; height: var(--input-height-default); @@ -33,86 +35,154 @@ font-family: var(--body-font); } -.testcase-controls { +/* Sample Controls */ +.sample-controls { display: flex; - gap: 1rem; - margin-bottom: 2rem; + gap: var(--spacing-small); + margin-bottom: var(--spacing-medium); align-items: center; background: var(--card-background); - padding: 1.5rem; - border-radius: var(--border-radius); + padding: var(--spacing-medium); + border-radius: var(--border-radius-small); border: 1px solid var(--border-color); } -.testcase-card { +#sampleCount { + width: 150px; + height: var(--input-height-default); + padding: var(--input-padding); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-small); + font-family: var(--body-font); +} + +#addSample { + margin-left: auto; +} + +/* Sample Cards */ +.sample-card { background: var(--card-background); - border-radius: var(--border-radius); - margin-bottom: 1.5rem; + border-radius: var(--border-radius-small); + margin-bottom: var(--spacing-medium); box-shadow: var(--shadow-default); border: 1px solid var(--border-color); } -.testcase-header { - padding: 1rem 1.5rem; +.sample-header { + padding: var(--spacing-small) var(--spacing-medium); cursor: pointer; display: flex; justify-content: space-between; align-items: center; - background: var(--background-secondary); + background: var(--background-color); border-bottom: 1px solid var(--border-color); - border-radius: var(--border-radius) var(--border-radius) 0 0; + border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; } -.testcase-content { - padding: 1.5rem; +.sample-content { + padding: var(--spacing-medium); } -.questions-section { - margin-top: 1.5rem; - border-top: 1px solid var(--border-color); - padding-top: 1.5rem; +/* Sample Inputs */ +.sample-inputs { + display: flex; + flex-direction: column; + gap: var(--spacing-medium); +} + +.free-variables { + background: var(--background-color); + padding: var(--spacing-medium); + border-radius: var(--border-radius-small); + border: 1px solid var(--border-color); +} + +.free-variables h4 { + margin-bottom: var(--spacing-small); + color: var(--text-color); + font-family: var(--heading-font); } -.questions-area { +/* Using form styles for inputs and textareas */ +.form-group textarea { width: 100%; - margin-bottom: 1rem; - min-height: 100px; resize: vertical; + min-height: 100px; padding: var(--input-padding); border: 1px solid var(--border-color); border-radius: var(--border-radius-small); font-family: var(--body-font); + background-color: var(--card-background); } -.input-variables { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 1rem; -} - -#testcaseCount { - width: 150px; - height: auto; +.form-group input { + width: 100%; + height: var(--input-height-default); padding: var(--input-padding); border: 1px solid var(--border-color); border-radius: var(--border-radius-small); font-family: var(--body-font); + background-color: var(--card-background); } -#addTestcase { - margin-left: auto; +/* Messages */ +.no-agent-message { + color: var(--secondary-color); + text-align: center; + padding: var(--spacing-small); } -.testcase-controls input, -.testcase-controls .button { - height: var(--input-height-default);; +/* Editor Actions */ +.editor-actions { display: flex; - align-items: center; + justify-content: flex-end; + margin-top: var(--spacing-medium); } -.testcase-actions { +/* Dataset Cards in List View */ +.dataset-card { + background: var(--card-background); + border-radius: var(--border-radius-small); + padding: var(--spacing-medium); + margin-bottom: var(--spacing-medium); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-default); +} + +.card__title { + margin: 0 0 var(--spacing-small); + color: var(--text-color); + font-size: 1.25rem; + font-family: var(--heading-font); +} + +.card__samples { + color: var(--secondary-color); + font-size: var(--font-size-small); + margin-bottom: var(--spacing-medium); +} + +.card__actions { display: flex; - justify-content: space-between; - margin-top: 1.5rem; + gap: var(--spacing-small); +} + +.card__button--edit, +.card__button--delete { + display: inline-flex; + align-items: center; + gap: calc(var(--spacing-small) / 2); + padding: calc(var(--spacing-small) / 2); + border-radius: var(--border-radius-small); + font-size: var(--font-size-small); +} + +.card__button--edit { + background: var(--background-color); +} + +.card__button--delete { + background-color: var(--accent-color); + color: var(--card-background); } \ No newline at end of file diff --git a/src/static/css/components/_forms.css b/src/static/css/components/_forms.css index 16970e5..209f65c 100644 --- a/src/static/css/components/_forms.css +++ b/src/static/css/components/_forms.css @@ -33,6 +33,7 @@ .form__range::-webkit-slider-runnable-track { height: var(--range-height); + background: var(--primary-color); border-radius: calc(var(--range-height) / 2); } diff --git a/src/static/js/api/datasets.js b/src/static/js/api/datasets.js index f18ccd9..b3e03bb 100644 --- a/src/static/js/api/datasets.js +++ b/src/static/js/api/datasets.js @@ -1,4 +1,15 @@ export const DatasetAPI = { + fetchById: async (id) => { + const response = await fetch(`/api/datasets/${id}`, { + method: 'GET', + headers: {'Content-Type': 'application/json'} + }); + if (!response.ok) { + throw new Error('Failed to fetch dataset'); + } + return response.json(); + }, + fetchAll: async () => { const response = await fetch('/api/datasets'); return response.json(); @@ -10,6 +21,21 @@ export const DatasetAPI = { headers: {'Content-Type': 'application/json'}, body: JSON.stringify(datasetData) }); + if (!response.ok) { + throw new Error('Failed to create dataset'); + } + return response.json(); + }, + + update: async (id, datasetData) => { + const response = await fetch(`/api/datasets/${id}`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(datasetData) + }); + if (!response.ok) { + throw new Error('Failed to update dataset'); + } return response.json(); }, diff --git a/src/static/js/components/dataset/datasetEditor.js b/src/static/js/components/dataset/datasetEditor.js index 59313b6..c9b9aa4 100644 --- a/src/static/js/components/dataset/datasetEditor.js +++ b/src/static/js/components/dataset/datasetEditor.js @@ -1,268 +1,381 @@ import { DatasetAPI } from "../../api/datasets.js"; +import { AiAgentAPI } from "../../api/aiAgents.js"; +import { VariableHelper } from "../../utils/variableHelper.js"; +import { UIHelper } from "../../utils/uiHelper.js"; export class DatasetEditor { constructor() { this.container = document.getElementById('datasetEditor'); - this.testcaseCount = 1; + this.sampleCount = 1; this.currentAgent = null; - this.promptVariables = new Set(); + this.freeVariables = new Set(); this.agents = new Map(); + this.sampleTemplate = null; this.init(); } - init() { + get isNewDataset() { + return window.location.hash === '#dataset/new'; + } + + async init() { this.render(); this.attachEventListeners(); + if (this.isNewDataset) { + const agents = await AiAgentAPI.fetchAll(); + this.handleAgentsUpdate(agents); + } } render() { this.container.innerHTML = `
-

Create Dataset

+

${this.isNewDataset ? 'Create' : 'Edit'} Dataset

-
- - -
-
- - +
+ +
+ ${this.isNewDataset ? ` +
+ + +
+ ` : ''}
-
-
- - - +
+
+ + + +
+
+ ${this.createSampleCard(1)}
-
- ${this.createTestcaseCard(1)} +
+
`; } - createTestcaseCard(index) { + createSampleCard(index, sample = null) { + const isFirstCard = index === 1; return ` -
-
- Testcase #${index} - expand_more +
+
+ Sample #${index} + ${isFirstCard ? 'expand_less' : 'expand_more'}
-
-
- ${this.currentAgent ? - Array.from(this.promptVariables).map(variable => ` -
- - -
- `).join('') : - '

Please select an agent first

' - } -
-
-

Questions

-
- - -
-
- - -
- -
- - ${index > 1 ? '' : ''} -
-
+
+ ${this.createSampleContent(sample)}
`; } - attachEventListeners() { - document.addEventListener('agents-updated', (event) => { - this.agents.clear(); - Object.entries(event.detail.agents).forEach(([id, agent]) => { - this.agents.set(id, agent); + createSampleContent(sample) { + if (!sample) { + if (this.isNewDataset && !this.currentAgent) { + return '

Please select an agent first

'; + } + if (!this.isNewDataset && !this.sampleTemplate) { + return '

Loading...

'; + } + sample = this.createEmptySample(); + } + return this.createSampleInputs(sample); + } + + createEmptySample() { + if (this.isNewDataset) { + return {}; + } + // Create empty sample based on template + const sample = {}; + if (this.sampleTemplate.free_variables) { + sample.free_variables = {}; + Object.keys(this.sampleTemplate.free_variables).forEach(key => { + sample.free_variables[key] = ''; }); - this.updateAgentDropdown(); - }); + } else if (this.sampleTemplate.user_prompt !== undefined) { + sample.user_prompt = ''; + } + return sample; + } - const backBtn = document.getElementById('backToList'); - backBtn.onclick = () => { - window.location.hash = '#datasets'; - }; + createSampleInputs(sample = {}) { + return ` +
+ ${this.isNewDataset ? + (this.freeVariables.size > 0 ? + this.createVariableInputs({ free_variables: Object.fromEntries([...this.freeVariables].map(v => [v, ''])) }) : + this.createUserPromptInput('')) : + (sample.free_variables ? + this.createVariableInputs(sample) : + this.createUserPromptInput(sample.user_prompt)) + } + ${this.createOptionalInputs(sample)} +
+ `; + } - const agentSelect = document.getElementById('agentSelect'); - agentSelect.addEventListener('change', (e) => this.handleAgentSelection(e.target.value)); - - const addTestcaseBtn = document.getElementById('addTestcase'); - addTestcaseBtn.onclick = () => { - this.testcaseCount++; - const testcasesList = document.getElementById('testcasesList'); - testcasesList.insertAdjacentHTML('beforeend', this.createTestcaseCard(this.testcaseCount)); - }; - - // Toggle testcase content - this.container.addEventListener('click', (e) => { - const header = e.target.closest('.testcase-header'); - if (header) { - const content = header.nextElementSibling; - content.style.display = content.style.display === 'none' ? 'block' : 'none'; - const icon = header.querySelector('.material-icons'); - icon.textContent = content.style.display === 'none' ? 'expand_more' : 'expand_less'; - } + createUserPromptInput(prompt = '') { + return ` +
+ + +
+ `; + } - // Handle remove testcase - if (e.target.classList.contains('remove-testcase')) { - const card = e.target.closest('.testcase-card'); - card.remove(); - this.testcaseCount--; - } - }); + createVariableInputs(sample) { + const variables = sample.free_variables ? + Object.entries(sample.free_variables) : + []; - // Handle generate testcases - const generateTestcasesBtn = document.getElementById('generateTestcases'); - generateTestcasesBtn.onclick = () => { - const count = parseInt(document.getElementById('testcaseCount').value); - if (count && count > 0) { - const testcasesList = document.getElementById('testcasesList'); - testcasesList.innerHTML = ''; - this.testcaseCount = count; - - for (let i = 1; i <= count; i++) { - testcasesList.insertAdjacentHTML('beforeend', this.createTestcaseCard(i)); - } - } - }; + return ` +
+ ${variables.map(([key, value = '']) => ` +
+ + +
+ `).join('')} +
+ `; + } + + createOptionalInputs(sample = {}) { + return ` +
+ + +
+
+ + +
+ `; + } + + attachEventListeners() { + document.getElementById('backToList').onclick = () => this.handleBackToList(); + document.getElementById('addSample').onclick = () => this.handleAddSample(); + document.getElementById('generateSamples').onclick = () => this.handleGenerateSamples(); + document.getElementById('saveDataset').onclick = () => this.handleSaveDataset(); + + if (this.isNewDataset) { + document.getElementById('agentSelect').onchange = (e) => this.handleAgentSelection(e.target.value); + } - this.container.addEventListener('click', async (e) => { - if (e.target.classList.contains('generate-questions')) { - const card = e.target.closest('.testcase-card'); - await this.handleGenerateQuestions(card); + this.attachSampleHeaderListeners(); + this.attachCustomEventListeners(); + } + + attachSampleHeaderListeners() { + document.getElementById('samplesList').addEventListener('click', (e) => { + const header = e.target.closest('.sample-header'); + if (header) { + this.toggleSampleContent(header); } }); } - - async handleGenerateQuestions(card) { - const questionsArea = card.querySelector('.questions-area'); - if (questionsArea.value.trim()) { - alert('Questions area must be empty to generate new questions'); - return; - } - - // Get all input values - const inputValues = {}; - const inputs = card.querySelectorAll('.input-variables input'); - let allFilled = true; + + attachCustomEventListeners() { + document.addEventListener('dataset-load', (event) => this.handleDatasetLoad(event.detail.dataset)); + document.addEventListener('agents-updated', (event) => this.handleAgentsUpdate(event.detail.agents)); + } + + handleBackToList() { + window.location.hash = '#datasets'; + } + + handleAddSample() { + this.sampleCount++; + const samplesList = document.getElementById('samplesList'); + samplesList.insertAdjacentHTML('beforeend', this.createSampleCard(this.sampleCount)); + } + + handleGenerateSamples() { + UIHelper.showToast('To be implemented', 'info'); + } + + toggleSampleContent(header) { + const content = header.nextElementSibling; + content.style.display = content.style.display === 'none' ? 'block' : 'none'; + const icon = header.querySelector('.material-icons'); + icon.textContent = content.style.display === 'none' ? 'expand_more' : 'expand_less'; + } + + handleDatasetLoad(dataset) { + document.getElementById('datasetName').value = dataset.name; + this.sampleTemplate = dataset.samples[0]; + this.regenerateSamplesList(dataset.samples); + } + + regenerateSamplesList(samples) { + const samplesList = document.getElementById('samplesList'); + samplesList.innerHTML = ''; + this.sampleCount = samples.length; - inputs.forEach(input => { - if (!input.value.trim()) { - allFilled = false; - } - inputValues[input.name] = input.value.trim(); + samples.forEach((sample, index) => { + samplesList.insertAdjacentHTML('beforeend', + this.createSampleCard(index + 1, sample) + ); }); - - if (!allFilled) { - alert('Please fill all input variables before generating questions'); - return; - } - - const numQuestions = card.querySelector('.question-count').value; - const guidelines = card.querySelector('.generation-guidelines').value.trim(); - const datasetType = document.getElementById('datasetType').value; - - try { - const response = await DatasetAPI.generateQuestions({ - agent: { - prompt: this.currentAgent.prompt[0].content, - config: this.currentAgent.config - }, - datasetType, - numQuestions, - guidelines, - inputs: inputValues - }); - - questionsArea.value = response.join('\n'); - } catch (error) { - console.error('Error generating questions:', error); - alert('Failed to generate questions. Please try again.'); - } + } + + handleAgentsUpdate(agents) { + this.agents = new Map(Object.entries(agents)); + this.updateAgentDropdown(); } handleAgentSelection(agentId) { if (!agentId) { this.currentAgent = null; - this.promptVariables.clear(); - this.updateTestcaseInputs(); - document.getElementById('addTestcase').disabled = true; + this.freeVariables.clear(); + this.updateSampleInputs(); return; } const agent = this.agents.get(agentId); if (agent) { this.currentAgent = agent; - this.promptVariables = this.extractVariables(agent.prompt[0].content); - this.updateTestcaseInputs(); - document.getElementById('addTestcase').disabled = false; + this.updateFreeVariables(agent); + this.updateSampleInputs(); } } - extractVariables(promptTemplate) { - const regex = /{([^}]+)}/g; - const matches = [...promptTemplate.matchAll(regex)]; - return new Set(matches.map(match => match[1])); - } - - updateTestcaseInputs() { - const inputContainers = document.querySelectorAll('.input-variables'); - inputContainers.forEach(container => { - container.innerHTML = this.currentAgent ? - Array.from(this.promptVariables).map(variable => ` -
- - -
- `).join('') : - '

Please select an agent first

'; + updateFreeVariables(agent) { + const allVariables = new Set(); + agent.prompt.forEach(prompt => { + const promptVariables = VariableHelper.extractVariables(prompt.content); + promptVariables.forEach(variable => allVariables.add(variable)); }); + + this.freeVariables = new Set( + [...allVariables].filter(variable => + !agent.included_variables?.includes(variable) + ) + ); } updateAgentDropdown() { const agentSelect = document.getElementById('agentSelect'); - if (agentSelect) { - agentSelect.innerHTML = ''; - this.agents.forEach((agent, id) => { - const option = document.createElement('option'); - option.value = id; - option.textContent = agent.name; - agentSelect.appendChild(option); - }); + agentSelect.innerHTML = ''; + + this.agents.forEach((agent, id) => { + const option = document.createElement('option'); + option.value = id; + option.textContent = agent.name; + agentSelect.appendChild(option); + }); + } + + updateSampleInputs() { + const samplesList = document.getElementById('samplesList'); + const samples = samplesList.querySelectorAll('.sample-card'); + + samples.forEach(sample => { + sample.querySelector('.sample-content').innerHTML = this.createSampleInputs({}); + }); + } + + async handleSaveDataset() { + const name = document.getElementById('datasetName').value; + if (!name) { + alert('Please enter a dataset name'); + return; + } + + const samples = this.collectSamples(); + if (samples.length === 0) { + alert('Please add at least one sample'); + return; + } + + try { + await this.saveDatasetToServer(name, samples); + window.location.hash = '#datasets'; + } catch (error) { + console.error('Error saving dataset:', error); + alert('Failed to save dataset'); + } + } + + collectSamples() { + const samples = []; + const sampleCards = document.querySelectorAll('.sample-card'); + + for (const card of sampleCards) { + const sample = this.collectSampleData(card); + if (Object.keys(sample).length > 0) { + samples.push(sample); + } + } + return samples; + } + + collectSampleData(card) { + const sample = {}; + + // Get user prompt or variables + const userPrompt = card.querySelector('.user-prompt'); + if (userPrompt && userPrompt.value) { + sample.user_prompt = userPrompt.value; + } else { + const variables = card.querySelectorAll('.variables input'); + if (variables.length > 0) { + sample.free_variables = {}; + variables.forEach(input => { + if (input.value) { + sample.free_variables[input.name] = input.value; + } + }); + } + } + + // Get optional fields + this.collectOptionalFields(card, sample); + return sample; + } + + collectOptionalFields(card, sample) { + const evalSchema = card.querySelector('.eval-schema'); + if (evalSchema && evalSchema.value) { + sample.evaluation_schema = evalSchema.value; + } + + const expectedOutput = card.querySelector('.expected-output'); + if (expectedOutput && expectedOutput.value) { + sample.expected_output = expectedOutput.value; + } + } + + async saveDatasetToServer(name, samples) { + const datasetData = { name, samples }; + const hash = window.location.hash; + + if (hash.startsWith('#dataset/') && hash !== '#dataset/new') { + const datasetId = hash.split('/')[1]; + await DatasetAPI.update(datasetId, datasetData); + UIHelper.showToast('Dataset updated successfully'); + } else { + await DatasetAPI.create(datasetData); + UIHelper.showToast('Dataset created successfully'); } } } \ No newline at end of file diff --git a/src/static/js/components/dataset/datasetManager.js b/src/static/js/components/dataset/datasetManager.js index 29aa7d9..4b084c8 100644 --- a/src/static/js/components/dataset/datasetManager.js +++ b/src/static/js/components/dataset/datasetManager.js @@ -5,8 +5,6 @@ export class DatasetManager { constructor() { this.list = document.getElementById('datasetList'); this.addBtn = document.getElementById('addDatasetBtn'); - this.editor = new DatasetEditor(); - this.listView = document.getElementById('datasetsListView'); this.editorView = document.getElementById('datasetEditor'); @@ -16,6 +14,10 @@ export class DatasetManager { // Handle hash changes for navigation window.addEventListener('hashchange', () => this.handleNavigation()); this.handleNavigation(); + + document.querySelector('a[href="#datasets"]').addEventListener('click', () => { + window.location.hash = '#datasets'; + }); } handleNavigation() { @@ -28,6 +30,10 @@ export class DatasetManager { } else if (hash.startsWith('#dataset/')) { this.listView.style.display = 'none'; this.editorView.style.display = 'block'; + + // Re-initialize editor for each navigation + this.editor = new DatasetEditor(); + const id = hash.split('/')[1]; if (id !== 'new') { // Load existing dataset @@ -45,8 +51,15 @@ export class DatasetManager { } async fetchAndRender() { + if (this.listView.style.display === 'none') { + return; + } + const datasets = await DatasetAPI.fetchAll(); - this.render(datasets); + if (!this.currentDatasets || JSON.stringify(datasets) !== JSON.stringify(this.currentDatasets)) { + this.currentDatasets = datasets; + this.render(datasets); + } document.dispatchEvent(new CustomEvent('datasets-updated', { detail: { datasets } @@ -65,21 +78,56 @@ export class DatasetManager { const card = document.createElement('div'); card.className = 'card'; card.innerHTML = ` -

${data.name}

-

Metric: ${data.metric_name}

-

Questions: ${data.questions.length}

- +
+

${data.name}

+

Samples: ${data.samples.length}

+
+
+ + +
`; - + + // Add click handlers + card.querySelector('.card__button--edit').onclick = () => this.showEditor(id); card.querySelector('.card__button--delete').onclick = () => this.deleteDataset(id); + return card; } + async loadDataset(id) { + try { + const dataset = await DatasetAPI.fetchById(id); + if (dataset) { + // Dispatch event to load the dataset into editor + document.dispatchEvent(new CustomEvent('dataset-load', { + detail: { dataset } + })); + } else { + alert('Dataset not found'); + window.location.hash = '#datasets'; + } + } catch (error) { + console.error('Error loading dataset:', error); + alert('Failed to load dataset'); + window.location.hash = '#datasets'; + } + } + async deleteDataset(id) { - await DatasetAPI.delete(id); - await this.fetchAndRender(); + if (confirm('Are you sure you want to delete this dataset?')) { + try { + await DatasetAPI.delete(id); + await this.fetchAndRender(); + } catch (error) { + console.error('Error deleting dataset:', error); + alert('Failed to delete dataset'); + } + } } showQuestionsPreview(data, questions) { diff --git a/src/static/js/components/navigation/navigation.js b/src/static/js/components/navigation/navigation.js index a6b112b..a350c5f 100644 --- a/src/static/js/components/navigation/navigation.js +++ b/src/static/js/components/navigation/navigation.js @@ -89,16 +89,26 @@ export class Navigation { } handleInitialRoute() { - // Get hash from URL or default to playground - const hash = window.location.hash.slice(1) || 'playground'; - const tabId = this.tabMapping[hash]; + // Get hash from URL + const hash = window.location.hash.slice(1); - if (tabId) { - // Activate the correct tab + // If hash starts with dataset, activate datasets tab + if (hash.startsWith('dataset')) { + const tabId = this.tabMapping['datasets']; this.activateTab(tabId); // Update sidenav selection - const sidenavItem = document.querySelector(`.sidenav__item[data-tab="${hash}"]`); + const datasetsTab = document.querySelector('.sidenav__item[data-tab="datasets"]'); + if (datasetsTab) { + this.tabItems.forEach(tab => tab.classList.remove('active')); + datasetsTab.classList.add('active'); + } + } else { + // Original behavior for other routes + const tabId = this.tabMapping[hash] || this.tabMapping['playground']; + this.activateTab(tabId); + + const sidenavItem = document.querySelector(`.sidenav__item[data-tab="${hash || 'playground'}"]`); if (sidenavItem) { this.tabItems.forEach(tab => tab.classList.remove('active')); sidenavItem.classList.add('active'); From db4a5a0225a62547fb88580407566f35297a0375 Mon Sep 17 00:00:00 2001 From: Bhavesh Sonalkar <78692256+BhaveshSonalkar@users.noreply.github.com> Date: Sun, 17 Nov 2024 18:47:46 +0530 Subject: [PATCH 07/62] DB Update * AI Agent Save and Load * Playground overhaul * formatting with 120 chars line break * added db_manager * reformatted * minor renaming * dict from str in promt * minor change * added shell context processor * mionr change * minor change refactor * minor fixes * added models in shell * added certifi * made picture nullable * made picture nullable * added custom field * fixes in custom field deserialization * added transaction support in CRUD base class * added pre-commit in requirements.txt * added ignore error code * optimized imports * using mongo engine * added shell context * added reverse_delete_rule=DENY * refactor * mongo engine connection added * added dataset type enum * format with pre-commit * added readme * user object creation added on login * modified as get_or_create does not exist * minor change * removed some field defaults * minor change * added datetime utils * revamped ai agent apis * revamped ai agent apis * added project routes * registered routes * minor fixes * dataset_routes modified * ai agents routes rename * casting object id to string * bug fixes * added url prefix * fixed prefix urls * fixed prefix urls * added serializers.py * moved AiAgent to ai_agent_services.py * added uniqueness constraint in User models.py * minor changes * removed internal management of history from LLMResponseService * minor change * using snake casing in urls * added streaming response service * modified ai agent routes and handled variant creation and removal logic * minor change * passing config in create app function * bug fix in indexes * fixes in crate app * prompt validation added * added logs * configured logging * removed imports * minor changes * minor changes * added serializer for variant * removed unused imports * added logs in auth * removed context as it is not used * bug fix * renamed MODELS to LLM_MODELS * removing firebase integration * api bug fix * removed models in core --------- Co-authored-by: aman-pandey-afk --- .DS_Store | Bin 0 -> 6148 bytes .flake8 | 3 + .pre-commit-config.yaml | 27 ++ README.md | 4 + server/.DS_Store | Bin 0 -> 6148 bytes server/__init__.py | 0 server/app/.DS_Store | Bin 0 -> 6148 bytes server/app/__init__.py | 50 ++-- server/app/api/aiagent_routes.py | 210 ++++++++++---- server/app/api/auth_routes.py | 59 +++- server/app/api/config_routes.py | 10 +- server/app/api/dataset_routes.py | 145 ++++------ server/app/api/evaluation_routes.py | 70 +---- server/app/api/main.py | 22 +- server/app/api/project_routes.py | 106 +++++++ server/app/auth/__init__.py | 13 + server/app/auth/decorators.py | 11 +- server/app/auth/handlers.py | 16 +- server/app/config.py | 77 ++++- server/app/core/aiagents.py | 55 ---- server/app/core/datasets.py | 119 -------- server/app/core/models.py | 124 -------- server/app/core/services/ai_agent_services.py | 20 ++ .../app/core/services/llm_response_service.py | 50 ++++ server/app/enums.py | 6 + server/app/firebase/__init__.py | 16 -- server/app/firebase/aiagent_dao.py | 120 -------- server/app/firebase/dataset_dao.py | 55 ---- server/app/firebase/evaluations_dao.py | 40 --- server/app/models.py | 79 ++++++ server/app/serializers.py | 28 ++ server/app/utils/__init__.py | 6 +- server/app/utils/api_clients.py | 102 +------ server/app/utils/datetime_utils.py | 8 + server/app/utils/exceptions.py | 11 - server/app/utils/model_map.py | 46 +-- server/app/utils/prompts.py | 2 +- server/requirements.txt | Bin 282 -> 442 bytes src/static/js/api/aiAgents.js | 10 +- src/static/js/components/aiAgentManager.js | 65 +++++ .../js/components/dataset/datasetManager.js | 4 +- src/static/js/components/datasetEditor.js | 268 ++++++++++++++++++ src/static/js/components/evaluationManager.js | 0 src/static/js/components/promptCard.js | 195 +++++++++++++ src/static/js/components/promptSettings.js | 117 ++++++++ 45 files changed, 1413 insertions(+), 956 deletions(-) create mode 100644 .DS_Store create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 server/.DS_Store create mode 100644 server/__init__.py create mode 100644 server/app/.DS_Store create mode 100644 server/app/api/project_routes.py delete mode 100644 server/app/core/aiagents.py create mode 100644 server/app/core/services/ai_agent_services.py create mode 100644 server/app/core/services/llm_response_service.py create mode 100644 server/app/enums.py delete mode 100644 server/app/firebase/__init__.py delete mode 100644 server/app/firebase/aiagent_dao.py delete mode 100644 server/app/firebase/evaluations_dao.py create mode 100644 server/app/models.py create mode 100644 server/app/serializers.py create mode 100644 server/app/utils/datetime_utils.py delete mode 100644 server/app/utils/exceptions.py create mode 100644 src/static/js/components/aiAgentManager.js create mode 100644 src/static/js/components/datasetEditor.js create mode 100644 src/static/js/components/evaluationManager.js create mode 100644 src/static/js/components/promptCard.js create mode 100644 src/static/js/components/promptSettings.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c5ae7c3dee0a6d2e53bff33745c9549def5ae239 GIT binary patch literal 6148 zcmeHKO>5gg5S_K1L?#q`NJ=gRL9QWn>yo7OqPpp!*Sa)^wm6bys9HrE^bsG7LFe|z z|eae%0HX&D!4P_VYI%hdknI2stE(az$0LQ_Xi)!7&xpgnymwa zxdH%Na2tWmzlEIRI}9Aw7BK@+HWg@7mAhgnn-0J4;sS@YMVn5_T|Sh1vvM~SW!{eZ zz6mE4Sd>v15C)bRXxhgPpa1XgzyB{M$&)Z33_L0ZRO@hX*vFFG*?MbneAfEVV<-#9 n)fTT)V3?yAv3wM7L5;xgvjGeo))wJ`$d7=bK?Y&qpEB?Vbwqk+ literal 0 HcmV?d00001 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..99f72c4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +extend-ignore = E203, E266, E501, W503, F403, F401, F841, F405 +max-line-length = 120 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9cb89dd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +files: ^server/.*\.py$ +repos: + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + args: ["--line-length=120"] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: detect-aws-credentials + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/pycqa/flake8 + rev: 7.1.1 + hooks: + - id: flake8 + +default_stages: [pre-commit, pre-push] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0e46d3 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Steps to install precommit +1. change directory to server and do `pip install -r requirements.txt` +2. now run `pre-commit install` to install precommit hooks +3. now you can run `pre-commit run --all-files` to run precommit hooks on all files if you want to run on specific file then run `pre-commit run ` \ No newline at end of file diff --git a/server/.DS_Store b/server/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..36475a8aa468376247dc92300605e1e3e2fc4531 GIT binary patch literal 6148 zcmeHKO;6iE5S>i|I6_E1v=SF2OI)KgkP2FHaRVH$qzn7^0X@w}BPI|xDxTy+x6AkOFirBJ)p}5sYHek8 z&0F)8b)H;plb`+l_gGtYSb*QrG1#SoBw0&-~}>3JL?l zfH1Hu47l^rTVIwBlJ5`(gn^}DfX{~j${0ErTq7jCtJ3%4I0Zr5-9U!%2k}r4$B)fkg(Yw(9f! zfBfhCf6+;5!hkUFUooI+N8O_iLUMQOi{SXK4WM74EIh8X_|pZ3xr#BCui{Op5wr(v W07HkRMYKTVAz)~bLKyg~3_JlRk!s!m literal 0 HcmV?d00001 diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/app/.DS_Store b/server/app/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..459c3a82168b00fffe55d03205eee82691e783f8 GIT binary patch literal 6148 zcmeHKOHRWu5Pg#dsZ}>!vO&rTDsh8Ql?7`K0JM}gQbi+57u{vg9k>GLV+C)bb`he{f$Ebj= zUEkKM!Xx(AZ+o>|O()CMgbdvt7xdwW8FODzHMW@Ij=sQ5`$?kSlBf>3HL5mC&JtEm z)`oj&k?>v;UXSChIPV9}d~UlgnYFwp5xGS?Vb<=e-NLqS(8BQ^o;)4{~90K}Zt zX3Xo)qQ(>v%ZN=tw$LPm5<{rOD~5z{))$diMr;Cxa7es-NE}(>4Ml8p_FotsQU>(Y z6>tUG3Uoqopy&VP&-ed!ke6HmSKwbMAo=)uJmM?Gv-RNR^sFuDw{$h-H34@a%-B&( ft{%mwbTif$njw}En}DpL`Hz6f;E5~ns|tJqQEqa? literal 0 HcmV?d00001 diff --git a/server/app/__init__.py b/server/app/__init__.py index d579843..dc95f75 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,31 +1,43 @@ +import logging + +import certifi +import mongoengine from flask import Flask from flask_cors import CORS -from .config import Config + +from .api.aiagent_routes import ai_agent_bp +from .api.auth_routes import auth_bp +from .api.config_routes import config_bp +from .api.dataset_routes import dataset_bp +from .api.main import main_bp +from .api.project_routes import projects_bp +from .config import Config, Logging + +# Initialize logging once +Logging.initialize(log_level=logging.INFO, log_file="application.log") + def create_app(config_class=Config): - app = Flask(__name__, - static_folder='../../src/static', - template_folder='../../src/templates') - + app = Flask(__name__, static_folder="../../src/static", template_folder="../../src/templates") + + # Connect to MongoDB + mongoengine.connect( + host=config_class.MONGO_URI, + ssl=True, + tlscafile=certifi.where(), + ) + # Load config app.config.from_object(config_class) - + # Initialize CORS CORS(app) - # Register blueprints - from .api.aiagent_routes import ai_agent_bp - from .api.dataset_routes import dataset_bp - from .api.evaluation_routes import evaluation_bp - from .api.config_routes import config_bp - from .api.main import main_bp - from .api.auth_routes import auth_bp - app.register_blueprint(main_bp) - app.register_blueprint(ai_agent_bp, url_prefix='/api') - app.register_blueprint(dataset_bp, url_prefix='/api') - app.register_blueprint(evaluation_bp, url_prefix='/api') - app.register_blueprint(config_bp, url_prefix='/api/config') - app.register_blueprint(auth_bp, url_prefix='/api') + app.register_blueprint(projects_bp, url_prefix="/api/projects") + app.register_blueprint(ai_agent_bp, url_prefix="/api/ai_agents") + app.register_blueprint(dataset_bp, url_prefix="/api/datasets") + app.register_blueprint(config_bp, url_prefix="/api/config") + app.register_blueprint(auth_bp, url_prefix="/api/auth") return app diff --git a/server/app/api/aiagent_routes.py b/server/app/api/aiagent_routes.py index b6e2345..685099c 100644 --- a/server/app/api/aiagent_routes.py +++ b/server/app/api/aiagent_routes.py @@ -1,66 +1,170 @@ -from flask import Blueprint, request, jsonify, session -from ..core.aiagents import ( - get_all_agents, save_agent, delete_agent, get_agent, - add_variant, delete_variant -) -from ..core.models import AiAgent +from bson import ObjectId +from flask import Blueprint, Response, jsonify, request, stream_with_context + from ..auth.decorators import login_required +from ..config import Logging +from ..core.services.ai_agent_services import generate_conversation_service +from ..models import AIAgent, AIAgentVariant, Project +from ..serializers import AIAgentSerializer, AIAgentVariantSerializer +from ..utils.datetime_utils import get_datetime_in_ist + +logger = Logging.get_logger(__name__) + +ai_agent_bp = Blueprint("ai_agents", __name__) -ai_agent_bp = Blueprint('ai_agents', __name__) -@ai_agent_bp.route('/ai-agents', methods=['GET']) +@ai_agent_bp.route("/", methods=["GET"]) @login_required -def get_agents(): - user_id = session['user']['uid'] - return jsonify(get_all_agents(user_id)) +def get_agents(project_id): + try: + project_id = ObjectId(project_id) + project = Project.objects.get(id=project_id) + ai_agents = AIAgent.objects.filter(project=project, is_alive=True) + serialized_agents = AIAgentSerializer(many=True).dump(ai_agents) + logger.info(f"Successfully fetched ai agents for the project: {str(project_id)}") + return ( + jsonify( + { + "message": f"Successfully fetched ai agents for the project: {str(project_id)}", + "data": serialized_agents, + } + ), + 200, + ) + except Exception as e: + logger.error(f"Error in fetching AI agents: {str(e)}") + return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route('/ai-agents', methods=['POST']) + +@ai_agent_bp.route("/", methods=["POST"]) @login_required def create_agent(): - data = request.json - user_id = session['user']['uid'] - - agent = AiAgent( - name=data['name'], - prompt=data['prompt'], - config=data['config'], - included_variables=data.get('included_variables', []) - ) - - agent_id = save_agent(agent, user_id) - return jsonify({ - "message": f"AI Agent '{data['name']}' created successfully", - "id": agent_id - }), 201 - -@ai_agent_bp.route('/ai-agents/', methods=['DELETE']) + try: + data = request.json + project_id_unsafe = data.get("project_id") + project_id = ObjectId(project_id_unsafe) + project = Project.objects.get(id=project_id) + agent = AIAgent( + project=project, + agent_config=data["config"], + prompt=data["prompt"], + included_variables=data["included_variables"], + is_alive=True, + added_on=get_datetime_in_ist(), + updated_on=get_datetime_in_ist(), + ).save() + logger.info("AI Agent created successfully") + return jsonify({"message": "AI Agent created successfully", "id": str(agent.id)}), 201 + except Exception as e: + logger.error(f"Error in creating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("/", methods=["PUT"]) +@login_required +def update_agent(agent_id): + try: + data = request.json + agent_id = ObjectId(agent_id) + agent = AIAgent.objects.get(id=agent_id) + agent.agent_config = data["config"] + agent.prompt = data["prompt"] + agent.included_variables = data["included_variables"] + agent.updated_on = get_datetime_in_ist() + agent.save() + logger.info("AI Agent updated successfully") + return jsonify({"message": "AI Agent updated successfully"}), 200 + except Exception as e: + logger.error(f"Error in updating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("/", methods=["DELETE"]) @login_required def remove_agent(agent_id): - user_id = session['user']['uid'] - if delete_agent(agent_id, user_id): - return jsonify({"message": "AI Agent deleted successfully"}) - return jsonify({"message": "AI Agent not found"}), 404 + try: + agent_id = ObjectId(agent_id) + agent = AIAgent.objects.get(id=agent_id) + agent.delete() + logger.info("AI Agent deleted successfully!") + return jsonify({"message": "AI Agent deleted successfully!"}), 200 + except Exception as e: + logger.error(f"Error in deleting AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + -@ai_agent_bp.route('/ai-agents//variants', methods=['POST']) +@ai_agent_bp.route("//variants", methods=["POST"]) @login_required def create_variant(agent_id): - data = request.json - user_id = session['user']['uid'] - variant_name = data['variant_name'] - variables = data['variables'] - - variant_id = add_variant(agent_id, user_id, variant_name, variables) - if variant_id: - return jsonify({ - "message": f"Variant '{variant_name}' added successfully", - "variant_id": variant_id - }) - return jsonify({"message": "Failed to add variant"}), 404 - -@ai_agent_bp.route('/ai-agents//variants/', methods=['DELETE']) + try: + data = request.json + variant_name = data["variant_name"] + variables = data["variables"] + agent = AIAgent.objects.get(id=ObjectId(agent_id)) + variable_names = variables.keys() + if not set(variable_names).issubset(set(agent.included_variables)): + return jsonify({"error": "Variables not included in the agent"}), 400 + + AIAgentVariant.objects.create( + agent=agent, + variant_name=variant_name, + variables=variables, + is_alive=True, + added_on=get_datetime_in_ist(), + updated_on=get_datetime_in_ist(), + ) + logger.info("Variant created successfully") + return jsonify({"message": "Variant created successfully"}), 201 + except Exception as e: + logger.error(f"Error in creating variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("//variants", methods=["GET"]) +@login_required +def get_variants(agent_id): + try: + agent = AIAgent.objects.get(id=ObjectId(agent_id)) + variants = AIAgentVariant.objects.filter(agent=agent, is_alive=True) + serialized_variants = AIAgentVariantSerializer(many=True).dump(variants) + logger.info("Successfully fetched variants") + return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 + except Exception as e: + logger.error(f"Error in fetching variants: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("/variants/", methods=["DELETE"]) @login_required -def remove_variant(agent_id, variant_name): - user_id = session['user']['uid'] - if delete_variant(agent_id, user_id, variant_name): - return jsonify({"message": "Variant deleted successfully"}) - return jsonify({"message": "Variant not found"}), 404 \ No newline at end of file +def remove_variant(variant_id): + try: + variant = AIAgentVariant.objects.get(variant_id=ObjectId(variant_id)) + variant.delete() + logger.info("Variant deleted successfully!") + return jsonify({"message": "Variant deleted successfully!"}), 200 + except Exception as e: + logger.error(f"Error in deleting variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("/conversation", methods=["POST"]) +@login_required +def generate_conversation(): + try: + data = request.json + config = data.get("config") + prompt = data.get("prompt") + stream = data.get("stream", True) + + if stream: + logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") + return Response( + stream_with_context(generate_conversation_service(config, prompt, stream)), + content_type="text/event-stream", + ) + else: + logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") + return jsonify(generate_conversation_service(config, prompt, stream)), 200 + except Exception as e: + logger.error(f"Error in generating conversation: {str(e)}") + return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/auth_routes.py b/server/app/api/auth_routes.py index acd05a6..3e5f048 100644 --- a/server/app/api/auth_routes.py +++ b/server/app/api/auth_routes.py @@ -1,34 +1,65 @@ -from flask import Blueprint, request, jsonify, session +from flask import Blueprint, jsonify, request, session + from ..auth.handlers import AuthHandler +from ..config import Logging +from ..models import User +from ..utils.datetime_utils import get_datetime_in_ist + +auth_bp = Blueprint("auth", __name__) +logger = Logging.get_logger(__name__) -auth_bp = Blueprint('auth', __name__) -@auth_bp.route('/auth/verify', methods=['POST']) +@auth_bp.route("/verify", methods=["POST"]) def verify_token(): """Verify Firebase token and create session""" data = request.get_json() - id_token = data.get('idToken') - + id_token = data.get("idToken") + if not id_token: - return jsonify({'error': 'No token provided'}), 400 + return jsonify({"error": "No token provided"}), 400 decoded_token = AuthHandler.verify_firebase_token(id_token) if not decoded_token: - return jsonify({'error': 'Invalid token'}), 401 + return jsonify({"error": "Invalid token"}), 401 + + # check if user exists in database + user = User.objects.filter(firebase_uid=decoded_token.get("uid")).first() + if not user: + # User does not exist, create a new one + logger.info(f"Creating new user: {decoded_token.get('email')}") + user = User( + name=decoded_token.get("name"), + email=decoded_token.get("email"), + picture=decoded_token.get("picture"), + firebase_uid=decoded_token.get("uid"), + added_on=get_datetime_in_ist(), + updated_on=get_datetime_in_ist(), + is_alive=True, + ) + user.save() + # if user is not active + if not user.is_alive: + logger.info(f"User account has been deactivated: {decoded_token.get('email')}") + return jsonify({"error": "User account has been deactivated"}), 401 + + logger.info(f"User logged in: {user.email}") AuthHandler.create_session(decoded_token) - return jsonify({'success': True, 'user': session['user']}) + return jsonify({"success": True, "user": session["user"]}) + -@auth_bp.route('/auth/logout', methods=['POST']) +@auth_bp.route("/logout", methods=["POST"]) def logout(): """Clear user session""" + logger.info(f"User logged out: {session.get('user').get('email')}") AuthHandler.clear_session() - return jsonify({'success': True}) + return jsonify({"success": True}) + -@auth_bp.route('/auth/session') +@auth_bp.route("/session") def get_session(): """Get current session info""" - user = session.get('user') + user = session.get("user") if user: - return jsonify({'user': user}) - return jsonify({'user': None}), 401 \ No newline at end of file + return jsonify({"user": user}) + return jsonify({"user": None}), 401 diff --git a/server/app/api/config_routes.py b/server/app/api/config_routes.py index d4eff01..e6e391c 100644 --- a/server/app/api/config_routes.py +++ b/server/app/api/config_routes.py @@ -1,8 +1,10 @@ from flask import Blueprint, jsonify -from ..utils.model_map import MODELS -config_bp = Blueprint('config', __name__) +from ..utils.model_map import LLM_MODELS -@config_bp.route('/models', methods=['GET']) +config_bp = Blueprint("config", __name__) + + +@config_bp.route("/models", methods=["GET"]) def get_models(): - return jsonify(MODELS) \ No newline at end of file + return jsonify(LLM_MODELS) diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index 34751b0..5bc8f7b 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -1,104 +1,81 @@ -from flask import Blueprint, request, jsonify, session -from ..core.datasets import Dataset, get_all_datasets, get_dataset +from bson import ObjectId +from flask import Blueprint, jsonify, request + from ..auth.decorators import login_required +from ..config import Logging +from ..models import Dataset, Project +from ..serializers import DatasetSerializer +from ..utils.datetime_utils import get_datetime_in_ist -dataset_bp = Blueprint('datasets', __name__) +logger = Logging.get_logger(__name__) -@dataset_bp.route('/datasets/', methods=['GET']) -@login_required -def get_dataset_by_id(dataset_id): - user_id = session['user']['uid'] - dataset = get_dataset(dataset_id, user_id) - if dataset: - return jsonify(dataset) - return jsonify({"message": "Dataset not found"}), 404 +dataset_bp = Blueprint("datasets", __name__) -@dataset_bp.route('/datasets', methods=['GET']) + +@dataset_bp.route("/", methods=["GET"]) @login_required -def get_datasets(): - user_id = session['user']['uid'] - return jsonify(get_all_datasets(user_id)) +def get_datasets(project_id): + try: + project_id = ObjectId(project_id) + project = Project.objects.get(id=project_id) + datasets = Dataset.objects.filter(project=project, is_alive=True) + serialized_datasets = DatasetSerializer( + many=True, + ).dump(datasets) + logger.info(f"Successfully fetched datasets for the project: {str(project_id)}") + return jsonify({"message": "Successfully fetched datasets", "data": serialized_datasets}) + except Exception as e: + logger.error(f"Error in fetching datasets: {str(e)}") + return jsonify({"message": str(e)}), 500 + -@dataset_bp.route('/datasets', methods=['POST']) +@dataset_bp.route("/", methods=["POST"]) @login_required def create_dataset(): - data = request.json - user_id = session['user']['uid'] - try: - # Create dataset instance - dataset = Dataset(name=data['name']) - - # Add samples - for sample in data['samples']: - dataset.add_sample(sample) - - # Save dataset - dataset_id = dataset.save(user_id) - - return jsonify({ - "message": f"Dataset '{data['name']}' created successfully", - "id": dataset_id - }), 201 - - except ValueError as e: - return jsonify({"message": str(e)}), 400 + data = request.json + project_id = ObjectId(data["project_id"]) + project = Project.objects.get(id=project_id) + dataset = Dataset( + project=project, + type=data["type"], + samples=data["samples"] or [], + is_alive=True, + added_on=get_datetime_in_ist(), + updated_on=get_datetime_in_ist(), + ).save() + logger.info(f"Dataset '{data['name']}' created successfully") + return jsonify({"message": f"Dataset '{data['name']}' created successfully", "id": str(dataset.id)}), 201 except Exception as e: - print(f"Error creating dataset: {str(e)}") - return jsonify({"message": "Failed to create dataset"}), 500 + logger.error(f"Error in creating dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 -@dataset_bp.route('/datasets/', methods=['PUT']) + +@dataset_bp.route("/", methods=["PUT"]) @login_required def update_dataset_route(dataset_id): - data = request.json - user_id = session['user']['uid'] - try: - # Create dataset instance - dataset = Dataset(name=data['name']) - - # Add samples - for sample in data['samples']: - dataset.add_sample(sample) - - # Update dataset - if dataset.update(dataset_id, user_id): - return jsonify({"message": "Dataset updated successfully"}) - return jsonify({"message": "Dataset not found"}), 404 - - except ValueError as e: - return jsonify({"message": str(e)}), 400 + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + dataset.samples = data.get("samples", dataset.samples) + dataset.updated_on = get_datetime_in_ist() + dataset.save() + logger.info(f"Dataset '{data['name']}' updated successfully") + return jsonify({"message": f"Dataset '{data['name']}' updated successfully"}) except Exception as e: - print(f"Error updating dataset: {str(e)}") - return jsonify({"message": "Failed to update dataset"}), 500 + logger.error(f"Error in updating dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 -@dataset_bp.route('/datasets/', methods=['DELETE']) -@login_required -def remove_dataset(dataset_id): - user_id = session['user']['uid'] - if Dataset.delete(dataset_id, user_id): - return jsonify({"message": "Dataset deleted successfully"}) - return jsonify({"message": "Dataset not found"}), 404 -@dataset_bp.route('/datasets/generate_questions', methods=['POST']) +@dataset_bp.route("/", methods=["DELETE"]) @login_required -def generate_questions(): +def remove_dataset(dataset_id): try: - data = request.json - - dataset_class = Dataset - dataset = dataset_class(name="temp", id="temp") - - questions = dataset.generate_questions( - agent_data=data['agent'], - guidelines=data['guidelines'], - num_questions=int(data['numQuestions']), - inputs=data.get('inputs', {}) - ) - return jsonify({"questions": questions}) - - except ValueError as e: - return jsonify({"message": str(e)}), 400 + Dataset.objects.get(id=dataset_id).delete() + logger.info("Dataset deleted successfully!") + return jsonify({"message": "Dataset deleted successfully! with id: " + dataset_id}), 200 except Exception as e: - print(f"Error in generate_questions endpoint: {str(e)}") - return jsonify({"message": "Failed to generate questions"}), 500 \ No newline at end of file + logger.error(f"Error in deleting dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 diff --git a/server/app/api/evaluation_routes.py b/server/app/api/evaluation_routes.py index e74a04d..9c5bb6d 100644 --- a/server/app/api/evaluation_routes.py +++ b/server/app/api/evaluation_routes.py @@ -1,69 +1 @@ -from flask import Blueprint, request, jsonify, session -from ..core.aiagents import get_agent -from ..core.datasets import get_dataset -from ..core.models import Evaluator -from ..auth.decorators import login_required - -evaluation_bp = Blueprint('evaluation', __name__) - -@evaluation_bp.route('/evaluate', methods=['POST']) -@login_required -def evaluate(): - data = request.json - user_id = session['user']['uid'] - character_id = data['character'] - dataset_id = data['dataset'] - - char_data = get_character(character_id, user_id) - if not char_data: - return jsonify({"message": "Character not found"}), 404 - - dataset = get_dataset(dataset_id, user_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - - character = Character(char_data["name"], char_data["backstory"], "User") - - evaluator = Evaluator( - metric={"name": dataset['metric_name'], "description": dataset['metric_description']}, - character=character, - n=len(dataset['questions']), - model_type="claude" if char_data["model"].startswith("claude") else "openai" - ) - - results = evaluator.test_model(dataset['questions']) - - return jsonify({ - "results": [ - {"question": q, "response": r, "score": s} - for q, r, s in results - ], - "total_score": sum(score for _, _, score in results), - "max_score": len(results) - }) - -@evaluation_bp.route('/generate_questions', methods=['POST']) -@login_required -def generate_questions(): - data = request.json - user_id = session['user']['uid'] - character_id = data['character'] - metric_name = data['metric_name'] - metric_description = data['metric_description'] - num_questions = int(data['num_questions']) - - char_data = get_character(character_id, user_id) - if not char_data: - return jsonify({"message": "Character not found"}), 404 - - character = Character(char_data["name"], char_data["backstory"], "User") - - evaluator = Evaluator( - metric={"name": metric_name, "description": metric_description}, - character=character, - n=num_questions, - model_type="claude" if char_data["model"].startswith("claude") else "openai" - ) - evaluator.generate_questions() - - return jsonify({"questions": evaluator.questions}) \ No newline at end of file +# to be filled diff --git a/server/app/api/main.py b/server/app/api/main.py index 94bc9c3..1ce101d 100644 --- a/server/app/api/main.py +++ b/server/app/api/main.py @@ -1,15 +1,17 @@ -from flask import Blueprint, render_template, session, redirect +from flask import Blueprint, redirect, render_template, session -main_bp = Blueprint('main', __name__) +main_bp = Blueprint("main", __name__) -@main_bp.route('/') + +@main_bp.route("/") def index(): - if 'user' not in session: - return redirect('/login') - return render_template('index.html') + if "user" not in session: + return redirect("/login") + return render_template("index.html") + -@main_bp.route('/login') +@main_bp.route("/login") def login(): - if 'user' in session: - return redirect('/') - return render_template('login.html') \ No newline at end of file + if "user" in session: + return redirect("/") + return render_template("login.html") diff --git a/server/app/api/project_routes.py b/server/app/api/project_routes.py new file mode 100644 index 0000000..f846c66 --- /dev/null +++ b/server/app/api/project_routes.py @@ -0,0 +1,106 @@ +from bson import ObjectId +from flask import Blueprint, jsonify, request, session + +from ..auth.decorators import login_required +from ..config import Logging +from ..models import Project, User +from ..serializers import ProjectSerializer +from ..utils.datetime_utils import get_datetime_in_ist + +logger = Logging.get_logger(__name__) + +projects_bp = Blueprint("projects", __name__) + + +@projects_bp.route("/", methods=["GET"]) +@login_required +def get_projects(): + try: + firebase_uid = session["user"]["uid"] + user = User.objects.get(firebase_uid=firebase_uid) + projects = Project.objects.filter(user=user, is_alive=True) + serialized_projects = ProjectSerializer(many=True).dump(projects) + logger.info(f"Successfully fetched projects for user: {firebase_uid}") + return jsonify( + { + "message": f"Successfully fetched projects for user_id: {str(user.id)}", + "data": serialized_projects, + } + ) + except Exception as e: + logger.error(f"Error in fetching projects: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@projects_bp.route("/", methods=["GET"]) +@login_required +def get_project(project_id): + try: + project_id = ObjectId(project_id) + project = Project.objects.get(id=project_id) + serialized_project = ProjectSerializer().dump(project) + logger.info(f"Successfully fetched project: {str(project_id)}") + return jsonify( + { + "message": f"Successfully fetched project: {str(project_id)}", + "data": serialized_project, + } + ) + except Exception as e: + logger.error(f"Error in fetching project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@projects_bp.route("/", methods=["POST"]) +@login_required +def create_project(): + try: + data = request.json + firebase_uid = session["user"]["uid"] + user = User.objects.get(firebase_uid=firebase_uid) + + project = Project( + user=user, + name=data["name"] or "Default Project", + description=data["description"] or "", + added_on=get_datetime_in_ist(), + updated_on=get_datetime_in_ist(), + is_alive=True, + ).save() + logger.info(f"Project '{data['name']}' created successfully") + return jsonify({"message": f"Project '{data['name']}' created successfully", "id": str(project.id)}), 201 + except Exception as e: + logger.error(f"Error in creating project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@projects_bp.route("/", methods=["PUT"]) +@login_required +def update_project(project_id): + try: + data = request.json + project_id = ObjectId(project_id) + project = Project.objects.get(id=project_id) + project.name = data["name"] + project.description = data["description"] + project.updated_on = get_datetime_in_ist() + project.save() + logger.info(f"Project '{data['name']}' updated successfully") + return jsonify({"message": f"Project '{data['name']}' updated successfully"}), 200 + except Exception as e: + logger.error(f"Error in updating project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@projects_bp.route("/", methods=["DELETE"]) +@login_required +def remove_project(project_id): + try: + project_id = ObjectId(project_id) + project = Project.objects.get(id=project_id) + project.delete() + logger.info("Project deleted successfully!") + return jsonify({"message": "Project deleted successfully!"}), 200 + except Exception as e: + logger.error(f"Error in deleting project: {str(e)}") + return jsonify({"error": str(e)}), 500 diff --git a/server/app/auth/__init__.py b/server/app/auth/__init__.py index e69de29..88e9e3b 100644 --- a/server/app/auth/__init__.py +++ b/server/app/auth/__init__.py @@ -0,0 +1,13 @@ +import firebase_admin +from firebase_admin import auth, credentials + +from ..config import Config + +# Initialize Firebase once +creds_path = Config.FIREBASE_CREDS +if not firebase_admin._apps: + cred = credentials.Certificate(creds_path) + firebase_admin.initialize_app(cred) + +# Create instances +auth_client = auth diff --git a/server/app/auth/decorators.py b/server/app/auth/decorators.py index 00fb2c0..ef4743b 100644 --- a/server/app/auth/decorators.py +++ b/server/app/auth/decorators.py @@ -1,10 +1,13 @@ from functools import wraps -from flask import session, jsonify + +from flask import jsonify, session + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): - if 'user' not in session: - return jsonify({'error': 'Authentication required'}), 401 + if "user" not in session: + return jsonify({"error": "Authentication required"}), 401 return f(*args, **kwargs) - return decorated_function \ No newline at end of file + + return decorated_function diff --git a/server/app/auth/handlers.py b/server/app/auth/handlers.py index 67030c1..c9ba01a 100644 --- a/server/app/auth/handlers.py +++ b/server/app/auth/handlers.py @@ -1,5 +1,7 @@ from flask import session -from ..firebase import auth_client + +from . import auth_client + class AuthHandler: @staticmethod @@ -15,14 +17,14 @@ def verify_firebase_token(id_token): @staticmethod def create_session(user_data): """Create user session after successful verification""" - session['user'] = { - 'uid': user_data['uid'], - 'email': user_data.get('email'), - 'name': user_data.get('name', ''), - 'picture': user_data.get('picture', '') + session["user"] = { + "uid": user_data["uid"], + "email": user_data.get("email"), + "name": user_data.get("name", ""), + "picture": user_data.get("picture", ""), } @staticmethod def clear_session(): """Clear user session""" - session.pop('user', None) \ No newline at end of file + session.pop("user", None) diff --git a/server/app/config.py b/server/app/config.py index 12201df..c8d46c6 100644 --- a/server/app/config.py +++ b/server/app/config.py @@ -1,38 +1,85 @@ +import logging import os from pathlib import Path +from typing import Optional + from dotenv import load_dotenv load_dotenv() + class Config: # Root directory (project root) ROOT_DIR = Path(__file__).parents[2] # Firebase config - FIREBASE_CREDS = ROOT_DIR / 'firebase-credentials.json' + FIREBASE_CREDS = ROOT_DIR / "firebase-credentials.json" + + # Mongo config + MONGO_URI = os.environ.get("MONGO_URI") # Flask config - SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key' + SECRET_KEY = os.environ.get("SECRET_KEY") or "dev-key" # API Keys - CLAUDE_API_KEY = os.environ.get('CLAUDE_API_KEY') - OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY') - + CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY") + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") + # File paths - FILE_DIR = ROOT_DIR / 'server' / 'data' # server/data directory - CHARACTERS_FILE = os.path.join(FILE_DIR, 'characters.json') - DATASETS_FILE = os.path.join(FILE_DIR, 'datasets.json') - + FILE_DIR = ROOT_DIR / "server" / "data" # server/data directory + CHARACTERS_FILE = os.path.join(FILE_DIR, "characters.json") + DATASETS_FILE = os.path.join(FILE_DIR, "datasets.json") + # Model defaults DEFAULT_CLAUDE_MODEL = "claude-3-5-sonnet-20240620" DEFAULT_OPENAI_MODEL = "gpt-4" DEFAULT_MAX_TOKENS = 4096 DEFAULT_TEMPERATURE = 0.7 - CORS_HEADERS = 'Content-Type' - + CORS_HEADERS = "Content-Type" + # If you're serving frontend from a different port in development - CORS_ORIGINS = [ - "http://localhost:5000", - "http://127.0.0.1:5000" - ] \ No newline at end of file + CORS_ORIGINS = ["http://localhost:5000", "http://127.0.0.1:5000"] + + +class Logging: + _is_configured = False + + @classmethod + def initialize(cls, log_level: int = logging.INFO, log_file: str = "app.log") -> None: + """ + Initializes the logging configuration. This method ensures logging is configured only once. + + Args: + log_level (int): Logging level (e.g., logging.INFO, logging.DEBUG). + log_file (str): Path to the log file. + """ + if cls._is_configured: + return + + logging.basicConfig( + level=log_level, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + handlers=[ + logging.FileHandler(log_file), # Log to a file + logging.StreamHandler(), # Log to the console + ], + ) + cls._is_configured = True + + @staticmethod + def get_logger(name: str, level: Optional[int] = None) -> logging.Logger: + """ + Retrieves a logger with the given name. Optionally adjusts the logging level for this logger. + + Args: + name (str): Name of the logger, typically `__name__`. + level (Optional[int]): Logging level for this logger (overrides global log level). + + Returns: + Logger: Configured logger instance. + """ + logger = logging.getLogger(name) + if level is not None: + logger.setLevel(level) + return logger diff --git a/server/app/core/aiagents.py b/server/app/core/aiagents.py deleted file mode 100644 index 7ddfbc6..0000000 --- a/server/app/core/aiagents.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Dict, Optional, Tuple -from ..firebase import aiagent_dao -from .models import AiAgent - -def get_all_agents(user_id: str) -> Dict: - """Get all AI agents for a user""" - return aiagent_dao.get_all(user_id) - -def save_agent(agent: AiAgent, user_id: str) -> str: - """Create new AI agent""" - return aiagent_dao.save(agent, user_id) - -def get_agent(agent_id: str, user_id: str, variant_name: str = None) -> Optional[AiAgent]: - """Get a specific AI agent and optionally a specific variant""" - data = aiagent_dao.get_by_id(agent_id, user_id, variant_name) - if data: - agent = AiAgent( - name=data['name'], - prompt=data['prompt'], - config=data['config'] - ) - - # Add variants if they exist - if 'variants' in data: - for var_name, var_data in data['variants'].items(): - agent.add_variant(var_name, var_data['variables']) - - return agent - return None - -def add_variant(agent_id: str, user_id: str, variant_name: str, variables: Dict[str, str]) -> bool: - """Add a new variant to an existing agent""" - # Get the base agent first - agent_data = aiagent_dao.get_by_id(agent_id, user_id) - if not agent_data: - return False - - # Validate that all variables are in included_variables - included_vars = set(agent_data.get('included_variables', [])) - if not included_vars: - return False - - variant_vars = set(variables.keys()) - if not variant_vars.issubset(included_vars): - return False - - return aiagent_dao.add_variant(agent_id, user_id, variant_name, variables) - -def delete_variant(agent_id: str, user_id: str, variant_name: str) -> bool: - """Delete a specific variant""" - return aiagent_dao.delete_variant(agent_id, user_id, variant_name) - -def delete_agent(agent_id: str, user_id: str) -> bool: - """Delete an AI agent""" - return aiagent_dao.delete(agent_id, user_id) \ No newline at end of file diff --git a/server/app/core/datasets.py b/server/app/core/datasets.py index 5752d32..e69de29 100644 --- a/server/app/core/datasets.py +++ b/server/app/core/datasets.py @@ -1,119 +0,0 @@ -import re -from typing import Dict, List -from ..firebase import dataset_dao -from ..utils.api_clients import LLMResponseService -from ..utils.prompts import subjective_input_sample_generation_template, input_testcases_generation_template, objective_input_output_sample_generation_template - -def get_all_datasets(user_id: str) -> Dict: - """Get all datasets for a user""" - return dataset_dao.get_all(user_id) - -def get_dataset(dataset_id: str, user_id: str) -> Dict: - """Get a specific dataset""" - return dataset_dao.get_by_id(dataset_id, user_id) - -class Dataset: - def __init__(self, name: str, id: str = None) -> None: - self.name = name - self.id = id - self.samples = [] # List of dictionaries - - def add_sample(self, sample: Dict) -> None: - """Add a sample to the dataset""" - if not (sample.get('free_variables') or sample.get('user_prompt')): - raise ValueError("Sample must contain either free variables or user prompt") - self.samples.append(sample) - - def validate_sample(self, sample: Dict) -> bool: - """Validate if a sample has required fields""" - return bool(sample.get('free_variables') or sample.get('user_prompt')) - - def save(self, user_id: str) -> str: - """Save the dataset to database""" - return dataset_dao.save( - name=self.name, - samples=self.samples, - user_id=user_id - ) - - def update(self, dataset_id: str, user_id: str) -> bool: - """Update the dataset in database""" - return dataset_dao.update( - dataset_id=dataset_id, - name=self.name, - samples=self.samples, - user_id=user_id - ) - - @staticmethod - def delete(dataset_id: str, user_id: str) -> bool: - """Delete a dataset""" - return dataset_dao.delete(dataset_id, user_id) - - def load(self, inputs): - self.inputs = inputs - - def format_prompt(self, prompt: str, inputs: dict) -> str: - """Format prompt with input variables""" - try: - return prompt.format(**inputs) - except KeyError as e: - raise ValueError(f"Missing required input variable: {str(e)}") - - def generate_questions(self, agent_data: dict, guidelines: str, num_questions: int, inputs: dict) -> list: - """Base method for generating questions""" - try: - formatted_prompt = self.format_prompt(agent_data['prompt'], inputs) - - questions = self.generate_samples( - model=agent_data['config']['model'], - agent_prompt=formatted_prompt, - user_request=guidelines, - num_q=num_questions - ) - - if not questions: - raise ValueError("No questions were generated") - - return questions - - except Exception as e: - print(f"Error generating questions: {str(e)}") - raise - - def generate_samples(self) -> list: - """Abstract method to be implemented by subclasses""" - raise NotImplementedError("Subclasses must implement generate_samples") - - def generate_input_testcases(self,model,prompt_template,num_test_cases,input_descriptions) -> list: - input_desc = "" - json_example = "" - for _input in input_descriptions: - if input_descriptions[_input] is not None: - input_desc += f"{_input}: {input_descriptions[_input]}, " - else: - input_desc += f"{_input}," - - json_example += f'"{_input}": ........,' - - message = input_testcases_generation_template.format( - agent_prompt_template = prompt_template, - input_desc = input_desc, - json_example = json_example, - num_test_cases = num_test_cases - ) - - service = LLMResponseService( - model = model, - system_prompt = "" - ) - - response = service.send_message(message) - - try: - input_testcase_json = eval(response) - return input_testcase_json - - except Exception as e: - print(e) - print("The LLM is probably not returning a valid JSON object, retry it probably") diff --git a/server/app/core/models.py b/server/app/core/models.py index 3048519..e69de29 100644 --- a/server/app/core/models.py +++ b/server/app/core/models.py @@ -1,124 +0,0 @@ -from ..utils import system_score -from .datasets import Dataset -from ..utils.api_clients import LLMResponseService -from ..utils.prompts import system_score -from typing import Dict, Optional - -class AgentVariant: - def __init__(self, id: str, name: str, variables: Dict[str, str]) -> None: - self.id = id - self.name = name - self.variables = variables - -class AiAgent: - def __init__(self, name: str, prompt: list[dict], config: dict, - inputs: dict = None, persist_history: bool = True, - variants: Dict[str, AgentVariant] = None, - included_variables: list[str] = None) -> None: - - self.name = name - self.raw_prompt = prompt - self.config = config - self.variants = variants or {} - self.included_variables = included_variables or [] - - # Process prompt with inputs if provided - self.process_prompt(inputs) - - # Initialize LLM service - self.service = LLMResponseService( - system_prompt=self.prompt[0]["content"], - persist_history=persist_history, - **self.config - ) - - def process_prompt(self, inputs: dict = None) -> None: - """Process prompt with given inputs""" - prompt_final = [] - inputs = inputs or {} - - for mess in self.raw_prompt: - if mess["role"] == "system": - system = mess["content"] - if "system" in inputs: - system = system.format(**inputs["system"]) - prompt_final.append({"role": "system", "content": system}) - - if mess["role"] == "user": - user = mess["content"] - if "user" in inputs: - user = user.format(**inputs["user"]) - prompt_final.append({"role": "user", "content": user}) - - self.prompt = prompt_final - - def add_variant(self, variant_id: str, name: str, variables: Dict[str, str]) -> None: - """Add a new variant with specific variable values""" - self.variants[variant_id] = AgentVariant( - id=variant_id, - name=name, - variables=variables - ) - - def get_variant(self, variant_id: str) -> Optional[AgentVariant]: - """Get a specific variant by ID""" - return self.variants.get(variant_id) - - def load_variant(self, variant_name: str) -> bool: - """Load a specific variant's inputs""" - variant = self.get_variant(variant_name) - if variant: - self.process_prompt(variant.variables) - return True - return False - - def send_message(self, user_query: str = ""): - service = self.service - if self.prompt[-1]["role"] == "user": - response = service.send_message(self.prompt[-1]) - else: - response = service.send_message(user_query) - - self.prompt = service.prompt - return response - -class LLMJudge(AiAgent): - - def __init__(self, name: str, config: dict,eval_desc: str, score_guide: str, prompt: list[dict] = None, inputs: dict = None) -> None: - - if prompt is not None: - system = system_score.format(eval_desc =eval_desc, score_guide = score_guide) - - prompt = [ - {"role":"system","content":system} - ] - super().__init__(name, prompt, config, inputs, persist_history = False) - - def score_response(self,user,response): - - user_query_score = f"User: {user} \nAssistant: {response}" - - score = super().send_message(user_query_score) - - return score - -class Evaluator: - - def __init__(self, agent: AiAgent ,dataset: Dataset) -> None: - self.dataset = dataset - self.agent = agent - - def auto_score(self, eval_scheme: str, score_guide: str, config: dict): - - scorer = LLMJudge(name = "default", config = config, eval_desc=eval_scheme, score_guide=score_guide) - - - ''' - Logic to iterate through the dataset's inputs and then send the messages to 'agent' and then using the scorer.score_response method to get the score - ''' - - def match_output_score(self): - ''' - Logic to iterate through the dataset and then send the inputs to 'agent' and then match the agent response to dataset output. - ''' - diff --git a/server/app/core/services/ai_agent_services.py b/server/app/core/services/ai_agent_services.py new file mode 100644 index 0000000..5d1672c --- /dev/null +++ b/server/app/core/services/ai_agent_services.py @@ -0,0 +1,20 @@ +import logging + +from server.app.config import Config, Logging +from server.app.core.services.llm_response_service import LLMResponseService + +logger = Logging.get_logger(__name__) + + +def generate_conversation_service(config: dict, prompt: list[dict], stream=False): + logger.info(f"Generating conversion for prompt: {prompt} and config: {config} with stream: {stream}") + model = config.get("model") + max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS + temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE + llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) + + logger.info(f"Generated conversion for prompt: {prompt} and config: {config} with stream: {stream}") + if stream: + return llm_service.get_streaming_response() + else: + return llm_service.get_response() diff --git a/server/app/core/services/llm_response_service.py b/server/app/core/services/llm_response_service.py new file mode 100644 index 0000000..937ed41 --- /dev/null +++ b/server/app/core/services/llm_response_service.py @@ -0,0 +1,50 @@ +from server.app.config import Config +from server.app.utils import get_response_claude, get_response_openai +from server.app.utils.model_map import get_model_type + +response_function_map = {"OpenAI": get_response_openai, "Anthropic": get_response_claude} + + +class LLMResponseService: + def __init__( + self, + model: str, + prompt: list[dict], + max_tokens: int = Config.DEFAULT_MAX_TOKENS, + temperature: float = Config.DEFAULT_TEMPERATURE, + ) -> None: + self.model_type = get_model_type(model) + self.response_function = response_function_map[self.model_type] + self.model = model + self.prompt = prompt + self.max_tokens = max_tokens + self.temperature = temperature + self._validate_prompt() + + def _validate_prompt(self): + if not self.prompt: + raise ValueError("Prompt is required") + if not isinstance(self.prompt, list): + raise ValueError("Prompt should be a list of dictionaries") + for p in self.prompt: + if not isinstance(p, dict): + raise ValueError("Prompt should be a list of dictionaries") + if "role" not in p: + raise ValueError("Prompt should have a role key") + if "content" not in p: + raise ValueError("Prompt should have a content key") + + def get_streaming_response(self): + response = self.response_function( + prompt=self.prompt, model=self.model, temp=self.temperature, max_tokens=self.max_tokens, stream=True + ) + full_response = "" + for chunk in response: + full_response += chunk + yield chunk + + def get_response(self): + response = self.response_function( + prompt=self.prompt, model=self.model, temp=self.temperature, max_tokens=self.max_tokens, stream=False + ) + return response diff --git a/server/app/enums.py b/server/app/enums.py new file mode 100644 index 0000000..9842d2e --- /dev/null +++ b/server/app/enums.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class DataSetTypes(Enum): + KEY_VALUE = "KEY_VALUE" + MULTI_TURN = "MULTITURN" diff --git a/server/app/firebase/__init__.py b/server/app/firebase/__init__.py deleted file mode 100644 index 383effb..0000000 --- a/server/app/firebase/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -import firebase_admin -from firebase_admin import credentials, firestore, auth -from ..config import Config - -# Initialize Firebase once -creds_path = Config.FIREBASE_CREDS -if not firebase_admin._apps: - cred = credentials.Certificate(creds_path) - firebase_admin.initialize_app(cred) - -# Create instances -db = firestore.client() -auth_client = auth - -# Export both services -__all__ = ['db', 'auth_client'] \ No newline at end of file diff --git a/server/app/firebase/aiagent_dao.py b/server/app/firebase/aiagent_dao.py deleted file mode 100644 index 63de421..0000000 --- a/server/app/firebase/aiagent_dao.py +++ /dev/null @@ -1,120 +0,0 @@ -from typing import Dict, Optional -from uuid import uuid4 -from . import db -from ..core.models import AiAgent, AgentVariant -from firebase_admin.firestore import SERVER_TIMESTAMP - -def get_all(user_id: str) -> Dict: - """Get all AI agents for a specific user""" - agents = {} - query = db.collection('ai_agents').where('user_id', '==', user_id).stream() - for doc in query: - agents[doc.id] = doc.to_dict() - return agents - -def get_by_id(agent_id: str, user_id: str, variant_id: str = None) -> Optional[Dict]: - """ - Get a specific AI agent if it belongs to the user - Optionally retrieve a specific variant - """ - doc_ref = db.collection('ai_agents').document(agent_id) - doc = doc_ref.get() - - if not doc.exists or doc.to_dict().get('user_id') != user_id: - return None - - agent_data = doc.to_dict() - - if variant_id and 'variants' in agent_data: - variant = agent_data['variants'].get(variant_id) - if variant: - # Merge variant data with base agent data - agent_data = {**agent_data, 'variables': variant['variables']} - - return agent_data - -def save(agent: AiAgent, user_id: str) -> str: - """Save new AI agent with user_id""" - doc_ref = db.collection('ai_agents').document() - - # Convert variants to proper structure with IDs - variants_dict = {} - for variant_id, variant in agent.variants.items(): - variants_dict[variant_id] = { - 'id': variant_id, - 'name': variant.name, - 'variables': variant.variables - } - - agent_data = { - "name": agent.name, - "prompt": agent.prompt, - "config": agent.config, - "user_id": user_id, - "created_at": SERVER_TIMESTAMP, - "included_variables": agent.included_variables, - "variants": variants_dict - } - - doc_ref.set(agent_data) - return doc_ref.id - -def add_variant(agent_id: str, user_id: str, variant_name: str, variables: Dict[str, str]) -> Optional[str]: - """ - Add a variant to an existing agent - Returns: variant_id if successful, None if failed - """ - doc_ref = db.collection('ai_agents').document(agent_id) - doc = doc_ref.get() - - if not doc.exists or doc.to_dict().get('user_id') != user_id: - return None - - # Get current variants to check for duplicate names - agent_data = doc.to_dict() - existing_variants = agent_data.get('variants', {}) - - # Check if variant name already exists - for variant in existing_variants.values(): - if variant['name'] == variant_name: - return None - - # Generate new variant ID - variant_id = str(uuid4()) - - doc_ref.update({ - f'variants.{variant_id}': { - 'id': variant_id, - 'name': variant_name, - 'variables': variables - } - }) - return variant_id - -def delete_variant(agent_id: str, user_id: str, variant_id: str) -> bool: - """Delete a specific variant from an agent""" - doc_ref = db.collection('ai_agents').document(agent_id) - doc = doc_ref.get() - - if not doc.exists or doc.to_dict().get('user_id') != user_id: - return False - - # Verify variant exists before deleting - agent_data = doc.to_dict() - if variant_id not in agent_data.get('variants', {}): - return False - - doc_ref.update({ - f'variants.{variant_id}': db.DELETE_FIELD - }) - return True - -def delete(agent_id: str, user_id: str) -> bool: - """Delete an AI agent if it belongs to the user""" - agent_ref = db.collection('ai_agents').document(agent_id) - doc = agent_ref.get() - - if doc.exists and doc.to_dict().get('user_id') == user_id: - agent_ref.delete() - return True - return False \ No newline at end of file diff --git a/server/app/firebase/dataset_dao.py b/server/app/firebase/dataset_dao.py index 043a805..e69de29 100644 --- a/server/app/firebase/dataset_dao.py +++ b/server/app/firebase/dataset_dao.py @@ -1,55 +0,0 @@ -from typing import Dict, Optional, List -from . import db - -def get_all(user_id: str) -> Dict: - """Get all datasets for a specific user""" - datasets = {} - query = db.collection('datasets').where('user_id', '==', user_id).stream() - for doc in query: - datasets[doc.id] = doc.to_dict() - return datasets - -def get_by_id(dataset_id: str, user_id: str) -> Optional[Dict]: - """Get a specific dataset if it belongs to the user""" - doc_ref = db.collection('datasets').document(dataset_id) - doc = doc_ref.get() - - if doc.exists and doc.to_dict().get('user_id') == user_id: - return doc.to_dict() - return None - -def save(name: str, samples: List[Dict], user_id: str) -> str: - """Save new dataset with user_id""" - doc_ref = db.collection('datasets').document() - - dataset_data = { - "name": name, - "samples": samples, - "user_id": user_id - } - - doc_ref.set(dataset_data) - return doc_ref.id - -def update(dataset_id: str, name: str, samples: List[Dict], user_id: str) -> bool: - """Update existing dataset if it belongs to the user""" - dataset_ref = db.collection('datasets').document(dataset_id) - doc = dataset_ref.get() - - if doc.exists and doc.to_dict().get('user_id') == user_id: - dataset_ref.update({ - "name": name, - "samples": samples - }) - return True - return False - -def delete(dataset_id: str, user_id: str) -> bool: - """Delete a dataset if it belongs to the user""" - dataset_ref = db.collection('datasets').document(dataset_id) - doc = dataset_ref.get() - - if doc.exists and doc.to_dict().get('user_id') == user_id: - dataset_ref.delete() - return True - return False \ No newline at end of file diff --git a/server/app/firebase/evaluations_dao.py b/server/app/firebase/evaluations_dao.py deleted file mode 100644 index 591452f..0000000 --- a/server/app/firebase/evaluations_dao.py +++ /dev/null @@ -1,40 +0,0 @@ -from firebase_admin import firestore -from typing import Dict, Optional, List -from datetime import datetime - -def create(evaluation_data: Dict) -> str: - """Create a new evaluation""" - db = firestore.client() - eval_ref = db.collection('evaluations').document() - - # Add timestamp - evaluation_data['created_at'] = datetime.now(datetime.UTC) - - eval_ref.set(evaluation_data) - return eval_ref.id - -def get_by_id(eval_id: str) -> Optional[Dict]: - """Get a specific evaluation by ID""" - db = firestore.client() - doc_ref = db.collection('evaluations').document(eval_id) - doc = doc_ref.get() - - if doc.exists: - return doc.to_dict() - return None - -def get_by_character(char_id: str) -> List[Dict]: - """Get all evaluations for a specific character""" - db = firestore.client() - evals_ref = db.collection('evaluations') - evals = evals_ref.where('character_id', '==', char_id).stream() - - return [eval_doc.to_dict() for eval_doc in evals] - -def get_by_dataset(dataset_id: str) -> List[Dict]: - """Get all evaluations for a specific dataset""" - db = firestore.client() - evals_ref = db.collection('evaluations') - evals = evals_ref.where('dataset_id', '==', dataset_id).stream() - - return [eval_doc.to_dict() for eval_doc in evals] \ No newline at end of file diff --git a/server/app/models.py b/server/app/models.py new file mode 100644 index 0000000..4a533ad --- /dev/null +++ b/server/app/models.py @@ -0,0 +1,79 @@ +# models.py +from mongoengine import ( + DENY, + BooleanField, + DateTimeField, + DictField, + Document, + EmailField, + ListField, + ReferenceField, + StringField, +) + +from server.app.enums import DataSetTypes + + +class User(Document): + name = StringField(required=True, min_length=1) + firebase_uid = StringField(required=True, unique=True) + picture = StringField() + email = EmailField(required=True, unique=True) + is_alive = BooleanField(default=True) + added_on = DateTimeField() + updated_on = DateTimeField() + + meta = { + "collection": "users", + "indexes": [ + { + "fields": ["firebase_uid"], + "unique": True, + } + ], + } + + +class Project(Document): + name = StringField(required=True, min_length=1) + user = ReferenceField(User, required=True, reverse_delete_rule=DENY) + description = StringField() + is_alive = BooleanField(default=True) + added_on = DateTimeField() + updated_on = DateTimeField() + + meta = {"collection": "projects"} + + +class AIAgent(Document): + project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) + agent_config = DictField(required=True) + prompt = ListField(DictField(required=True)) + included_variables = ListField(StringField()) + is_alive = BooleanField(default=True) + added_on = DateTimeField() + updated_on = DateTimeField() + + meta = {"collection": "ai_agents"} + + +class AIAgentVariant(Document): + agent = ReferenceField(AIAgent, required=True, reverse_delete_rule=DENY) + name = StringField(required=True) + variables = DictField(required=True) + is_alive = BooleanField(default=True) + added_on = DateTimeField() + updated_on = DateTimeField() + + meta = {"collection": "ai_agent_variants"} + + +class Dataset(Document): + project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) + type = StringField(required=True, choices=[item.value for item in DataSetTypes]) + samples = ListField(DictField(), required=True) + is_alive = BooleanField(default=True) + added_on = DateTimeField() + updated_on = DateTimeField() + + meta = {"collection": "datasets"} diff --git a/server/app/serializers.py b/server/app/serializers.py new file mode 100644 index 0000000..a820ac8 --- /dev/null +++ b/server/app/serializers.py @@ -0,0 +1,28 @@ +from marshmallow import Schema + +from server.app.models import * + + +class UserSerializer(Schema): + class Meta: + model = User + + +class ProjectSerializer(Schema): + class Meta: + model = Project + + +class AIAgentSerializer(Schema): + class Meta: + model = AIAgent + + +class AIAgentVariantSerializer(Schema): + class Meta: + model = AIAgentVariant + + +class DatasetSerializer(Schema): + class Meta: + model = Dataset diff --git a/server/app/utils/__init__.py b/server/app/utils/__init__.py index 0904a4d..24e7f2a 100644 --- a/server/app/utils/__init__.py +++ b/server/app/utils/__init__.py @@ -1,8 +1,4 @@ from .api_clients import get_response_claude, get_response_openai from .prompts import system_score -__all__ = [ - 'get_response_claude', - 'get_response_openai', - 'system_score' -] \ No newline at end of file +__all__ = ["get_response_claude", "get_response_openai", "system_score"] diff --git a/server/app/utils/api_clients.py b/server/app/utils/api_clients.py index fb1de85..0a5adeb 100644 --- a/server/app/utils/api_clients.py +++ b/server/app/utils/api_clients.py @@ -1,7 +1,8 @@ from anthropic import Anthropic from openai import OpenAI + from ..config import Config -from .model_map import get_model_type + def get_response_claude_stream(message): """Helper function to handle streaming responses""" @@ -9,12 +10,13 @@ def get_response_claude_stream(message): if response.type == "content_block_delta": yield response.delta.text + def get_response_claude( prompt: list, model: str = Config.DEFAULT_CLAUDE_MODEL, temp: float = Config.DEFAULT_TEMPERATURE, max_tokens: int = Config.DEFAULT_MAX_TOKENS, - stream: bool = False + stream: bool = False, ): client = Anthropic(api_key=Config.CLAUDE_API_KEY) message = client.messages.create( @@ -23,117 +25,35 @@ def get_response_claude( temperature=temp, system=prompt[0]["content"], messages=prompt[1:], - stream=stream + stream=stream, ) if stream: return get_response_claude_stream(message) - + return message.content[0].text + def get_response_openai_stream(completion): """Helper function to handle OpenAI streaming responses""" for response in completion: yield response.choices[0].delta.content or "" + def get_response_openai( prompt: list, model: str = Config.DEFAULT_OPENAI_MODEL, max_tokens: int = Config.DEFAULT_MAX_TOKENS, temp: float = Config.DEFAULT_TEMPERATURE, - stream: bool = False + stream: bool = False, ): client = OpenAI(api_key=Config.OPENAI_API_KEY) completion = client.chat.completions.create( - model=model, - messages=prompt, - temperature=temp, - max_tokens=max_tokens, - stream=stream + model=model, messages=prompt, temperature=temp, max_tokens=max_tokens, stream=stream ) if stream: return get_response_openai_stream(completion) - - return completion.choices[0].message.content -response_function_map = { - "OpenAI": get_response_openai, - "Anthropic": get_response_claude -} - -class LLMResponseService(): - - def __init__( - self, - model: str, - system_prompt: str, - max_tokens: int = Config.DEFAULT_MAX_TOKENS, - temperature: float = Config.DEFAULT_TEMPERATURE, - persist_history: bool = True, - stream: bool = False - ) -> None: - self.model_type = get_model_type(model) - self.response_function = response_function_map[self.model_type] - self.model = model - self.system_prompt = system_prompt - self.max_tokens = max_tokens - self.temperature = temperature - self.persist_history = persist_history - self.stream = stream - - # Initialize prompt with system message - self.prompt = [{"role": "system", "content": system_prompt}] - if self.stream: - self.send_message = self._send_message_stream - else: - self.send_message = self._send_message_normal - - def _send_message_stream(self, user_query: str): - prompt = self.prompt.copy() - query_object = {"role": "user", "content": user_query} - prompt.append(query_object) - - response = self.response_function( - prompt=prompt, - model=self.model, - temp=self.temperature, - max_tokens=self.max_tokens, - stream=True - ) - - full_response = "" - for chunk in response: - full_response += chunk - yield chunk - - if self.persist_history: - self.prompt.append(query_object) - self.prompt.append({ - "role": "assistant", - "content": full_response - }) - - - def _send_message_normal(self, user_query: str): - prompt = self.prompt.copy() - query_object = {"role": "user", "content": user_query} - prompt.append(query_object) - - response = self.response_function( - prompt=prompt, - model=self.model, - temp=self.temperature, - max_tokens=self.max_tokens, - stream=False - ) - - if self.persist_history: - self.prompt.append(query_object) - self.prompt.append({ - "role": "assistant", - "content": response - }) - - return response \ No newline at end of file + return completion.choices[0].message.content diff --git a/server/app/utils/datetime_utils.py b/server/app/utils/datetime_utils.py new file mode 100644 index 0000000..9b1f2f8 --- /dev/null +++ b/server/app/utils/datetime_utils.py @@ -0,0 +1,8 @@ +from datetime import datetime, timedelta, timezone + + +def get_datetime_in_ist(datetime_obj=None): + """Converts a UTC datetime object to IST timezone""" + if not datetime_obj: + datetime_obj = datetime.now() + return datetime_obj.astimezone(timezone(timedelta(hours=5, minutes=30))) diff --git a/server/app/utils/exceptions.py b/server/app/utils/exceptions.py deleted file mode 100644 index 1d143b3..0000000 --- a/server/app/utils/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -class APIError(Exception): - """Base exception for API errors""" - pass - -class ModelError(APIError): - """Raised when there's an error with the AI model response""" - pass - -class ValidationError(APIError): - """Raised when there's an error with input validation""" - pass \ No newline at end of file diff --git a/server/app/utils/model_map.py b/server/app/utils/model_map.py index 5296020..ec8cf68 100644 --- a/server/app/utils/model_map.py +++ b/server/app/utils/model_map.py @@ -1,40 +1,20 @@ -MODELS = { +LLM_MODELS = { "OpenAI": [ - { - "id": "gpt-4o", - "name": "GPT-4" - }, - { - "id": "gpt-4-turbo", - "name": "GPT-4 Turbo" - }, - { - "id": "gpt-3.5-turbo", - "name": "GPT-3.5 Turbo" - }, - { - "id": "gpt-4o-mini", - "name": "GPT-4 Mini" - }, - { - "id": "gpt-4-1106-preview", - "name": "GPT-4 Preview" - } + {"id": "gpt-4o", "name": "GPT-4"}, + {"id": "gpt-4-turbo", "name": "GPT-4 Turbo"}, + {"id": "gpt-3.5-turbo", "name": "GPT-3.5 Turbo"}, + {"id": "gpt-4o-mini", "name": "GPT-4 Mini"}, + {"id": "gpt-4-1106-preview", "name": "GPT-4 Preview"}, ], "Anthropic": [ - { - "id": "claude-3-5-sonnet-20241022", - "name": "Claude 3 Sonnet" - }, - { - "id": "claude-3-5-haiku-20241022", - "name": "Claude 3 Haiku" - } - ] + {"id": "claude-3-5-sonnet-20241022", "name": "Claude 3 Sonnet"}, + {"id": "claude-3-5-haiku-20241022", "name": "Claude 3 Haiku"}, + ], } + def get_model_type(model): - for provider, models in MODELS.items(): - if any(m['id'] == model for m in models): + for provider, models in LLM_MODELS.items(): + if any(m["id"] == model for m in models): return provider - return None \ No newline at end of file + return None diff --git a/server/app/utils/prompts.py b/server/app/utils/prompts.py index 686c68c..471dd07 100644 --- a/server/app/utils/prompts.py +++ b/server/app/utils/prompts.py @@ -87,4 +87,4 @@ LLM_as_a_judge_template = """ You are acting as an LLM as a judge -""" \ No newline at end of file +""" diff --git a/server/requirements.txt b/server/requirements.txt index 1cf0439e910a296b4f3abaf52050d096b2a71725..15c284dbbf3627340a5f32122d006a8252cd4a01 100644 GIT binary patch delta 169 zcmXBM!3}~y5QX6%yqVYlHwuuz;sFj6W6UZfEEpy6NFP?=nd*2uX7XlcvtQ<$Ziio% ztbr>p-t@$6RLy+oR5b<8dc}z&H@xJ+z+H*LQ7RX?Myd6R?kVLt%g?$QWEaz&$x`xv VFX(^Dd1!bh%|YE8jmXg&-2s278cYBH delta 7 OcmdnRJd0_A6e9o%egY)` diff --git a/src/static/js/api/aiAgents.js b/src/static/js/api/aiAgents.js index 5c20a3d..fefdf76 100644 --- a/src/static/js/api/aiAgents.js +++ b/src/static/js/api/aiAgents.js @@ -1,11 +1,11 @@ export const AiAgentAPI = { fetchAll: async () => { - const response = await fetch('/api/ai-agents'); + const response = await fetch('/api/ai_agents'); return response.json(); }, create: async (agentData) => { - const response = await fetch('/api/ai-agents', { + const response = await fetch('/api/ai_agents', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(agentData) @@ -14,14 +14,14 @@ export const AiAgentAPI = { }, delete: async (id) => { - const response = await fetch(`/api/ai-agents/${id}`, { + const response = await fetch(`/api/ai_agents/${id}`, { method: 'DELETE' }); return response.json(); }, addVariant: async (agentId, variantData) => { - const response = await fetch(`/api/ai-agents/${agentId}/variants`, { + const response = await fetch(`/api/ai_agents/${agentId}/variants`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(variantData) @@ -30,7 +30,7 @@ export const AiAgentAPI = { }, deleteVariant: async (agentId, variantId) => { - const response = await fetch(`/api/ai-agents/${agentId}/variants/${variantId}`, { + const response = await fetch(`/api/ai_agents/${agentId}/variants/${variantId}`, { method: 'DELETE' }); return response.json(); diff --git a/src/static/js/components/aiAgentManager.js b/src/static/js/components/aiAgentManager.js new file mode 100644 index 0000000..4f2495f --- /dev/null +++ b/src/static/js/components/aiAgentManager.js @@ -0,0 +1,65 @@ +import { AiAgentAPI } from '../api/aiAgents.js'; + +export class AiAgentManager { + constructor() { + this.list = document.getElementById('agentList'); + this.fetchAndRender() + + // Listen for save events from PromptCards + document.addEventListener('save-agent', async (event) => { + await this.saveAgent(event.detail.agent); + }); + } + + async saveAgent(agentData) { + try { + await AiAgentAPI.create(agentData); + await this.fetchAndRender(); + } catch (error) { + console.error('Failed to save agent:', error); + alert('Failed to save agent'); + } + } + + async fetchAndRender() { + const agents = await AiAgentAPI.fetchAll(); + this.render(agents); + + document.dispatchEvent(new CustomEvent('agents-updated', { + detail: { agents } + })); + } + + render(agents) { + this.list.innerHTML = ''; + for (const [id, data] of Object.entries(agents)) { + const card = this.createAgentCard(id, data); + this.list.appendChild(card); + } + } + + createAgentCard(id, data) { + const card = document.createElement('div'); + card.className = 'card'; + card.innerHTML = ` +

${data.name}

+
+

Prompt:

+
${data.prompt[0].content}
+

Configuration:

+
${JSON.stringify(data.config, null, 2)}
+
+ + `; + + card.querySelector('.card__button--delete').onclick = () => this.deleteAgent(id); + return card; + } + + async deleteAgent(id) { + await AiAgentAPI.delete(id); + await this.fetchAndRender(); + } +} \ No newline at end of file diff --git a/src/static/js/components/dataset/datasetManager.js b/src/static/js/components/dataset/datasetManager.js index 4b084c8..33d3701 100644 --- a/src/static/js/components/dataset/datasetManager.js +++ b/src/static/js/components/dataset/datasetManager.js @@ -10,7 +10,7 @@ export class DatasetManager { // Update add button click handler this.addBtn.onclick = () => this.showEditor(); - + // Handle hash changes for navigation window.addEventListener('hashchange', () => this.handleNavigation()); this.handleNavigation(); @@ -22,7 +22,7 @@ export class DatasetManager { handleNavigation() { const hash = window.location.hash || '#datasets'; - + if (hash === '#datasets') { this.listView.style.display = 'block'; this.editorView.style.display = 'none'; diff --git a/src/static/js/components/datasetEditor.js b/src/static/js/components/datasetEditor.js new file mode 100644 index 0000000..907b84f --- /dev/null +++ b/src/static/js/components/datasetEditor.js @@ -0,0 +1,268 @@ +import { DatasetAPI } from "../api/datasets.js"; + +export class DatasetEditor { + constructor() { + this.container = document.getElementById('datasetEditor'); + this.testcaseCount = 1; + this.currentAgent = null; + this.promptVariables = new Set(); + this.agents = new Map(); + this.init(); + } + + init() { + this.render(); + this.attachEventListeners(); + } + + render() { + this.container.innerHTML = ` +
+

Create Dataset

+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + + +
+
+ ${this.createTestcaseCard(1)} +
+
+
+ `; + } + + createTestcaseCard(index) { + return ` +
+
+ Testcase #${index} + expand_more +
+
+
+ ${this.currentAgent ? + Array.from(this.promptVariables).map(variable => ` +
+ + +
+ `).join('') : + '

Please select an agent first

' + } +
+
+

Questions

+
+ + +
+
+ + +
+ +
+ + ${index > 1 ? '' : ''} +
+
+
+
+ `; + } + + attachEventListeners() { + document.addEventListener('agents-updated', (event) => { + this.agents.clear(); + Object.entries(event.detail.agents).forEach(([id, agent]) => { + this.agents.set(id, agent); + }); + this.updateAgentDropdown(); + }); + + const backBtn = document.getElementById('backToList'); + backBtn.onclick = () => { + window.location.hash = '#datasets'; + }; + + const agentSelect = document.getElementById('agentSelect'); + agentSelect.addEventListener('change', (e) => this.handleAgentSelection(e.target.value)); + + const addTestcaseBtn = document.getElementById('addTestcase'); + addTestcaseBtn.onclick = () => { + this.testcaseCount++; + const testcasesList = document.getElementById('testcasesList'); + testcasesList.insertAdjacentHTML('beforeend', this.createTestcaseCard(this.testcaseCount)); + }; + + // Toggle testcase content + this.container.addEventListener('click', (e) => { + const header = e.target.closest('.testcase-header'); + if (header) { + const content = header.nextElementSibling; + content.style.display = content.style.display === 'none' ? 'block' : 'none'; + const icon = header.querySelector('.material-icons'); + icon.textContent = content.style.display === 'none' ? 'expand_more' : 'expand_less'; + } + + // Handle remove testcase + if (e.target.classList.contains('remove-testcase')) { + const card = e.target.closest('.testcase-card'); + card.remove(); + this.testcaseCount--; + } + }); + + // Handle generate testcases + const generateTestcasesBtn = document.getElementById('generateTestcases'); + generateTestcasesBtn.onclick = () => { + const count = parseInt(document.getElementById('testcaseCount').value); + if (count && count > 0) { + const testcasesList = document.getElementById('testcasesList'); + testcasesList.innerHTML = ''; + this.testcaseCount = count; + + for (let i = 1; i <= count; i++) { + testcasesList.insertAdjacentHTML('beforeend', this.createTestcaseCard(i)); + } + } + }; + + this.container.addEventListener('click', async (e) => { + if (e.target.classList.contains('generate-questions')) { + const card = e.target.closest('.testcase-card'); + await this.handleGenerateQuestions(card); + } + }); + } + + async handleGenerateQuestions(card) { + const questionsArea = card.querySelector('.questions-area'); + if (questionsArea.value.trim()) { + alert('Questions area must be empty to generate new questions'); + return; + } + + // Get all input values + const inputValues = {}; + const inputs = card.querySelectorAll('.input-variables input'); + let allFilled = true; + + inputs.forEach(input => { + if (!input.value.trim()) { + allFilled = false; + } + inputValues[input.name] = input.value.trim(); + }); + + if (!allFilled) { + alert('Please fill all input variables before generating questions'); + return; + } + + const numQuestions = card.querySelector('.question-count').value; + const guidelines = card.querySelector('.generation-guidelines').value.trim(); + const datasetType = document.getElementById('datasetType').value; + + try { + const response = await DatasetAPI.generateQuestions({ + agent: { + prompt: this.currentAgent.prompt[0].content, + config: this.currentAgent.config + }, + datasetType, + numQuestions, + guidelines, + inputs: inputValues + }); + + questionsArea.value = response.join('\n'); + } catch (error) { + console.error('Error generating questions:', error); + alert('Failed to generate questions. Please try again.'); + } + } + + handleAgentSelection(agentId) { + if (!agentId) { + this.currentAgent = null; + this.promptVariables.clear(); + this.updateTestcaseInputs(); + document.getElementById('addTestcase').disabled = true; + return; + } + + const agent = this.agents.get(agentId); + if (agent) { + this.currentAgent = agent; + this.promptVariables = this.extractVariables(agent.prompt[0].content); + this.updateTestcaseInputs(); + document.getElementById('addTestcase').disabled = false; + } + } + + extractVariables(promptTemplate) { + const regex = /{([^}]+)}/g; + const matches = [...promptTemplate.matchAll(regex)]; + return new Set(matches.map(match => match[1])); + } + + updateTestcaseInputs() { + const inputContainers = document.querySelectorAll('.input-variables'); + inputContainers.forEach(container => { + container.innerHTML = this.currentAgent ? + Array.from(this.promptVariables).map(variable => ` +
+ + +
+ `).join('') : + '

Please select an agent first

'; + }); + } + + updateAgentDropdown() { + const agentSelect = document.getElementById('agentSelect'); + if (agentSelect) { + agentSelect.innerHTML = ''; + this.agents.forEach((agent, id) => { + const option = document.createElement('option'); + option.value = id; + option.textContent = agent.name; + agentSelect.appendChild(option); + }); + } + } +} \ No newline at end of file diff --git a/src/static/js/components/evaluationManager.js b/src/static/js/components/evaluationManager.js new file mode 100644 index 0000000..e69de29 diff --git a/src/static/js/components/promptCard.js b/src/static/js/components/promptCard.js new file mode 100644 index 0000000..fb3f380 --- /dev/null +++ b/src/static/js/components/promptCard.js @@ -0,0 +1,195 @@ +import { PromptSettings } from "./promptSettings.js"; + +export class PromptCard { + constructor(agents) { + this.agents = agents; + this.element = document.createElement('div'); + this.element.className = 'prompt-card'; + this.promptSettings = new PromptSettings(); + this.render(); + this.initializeEventListeners(); + + this.chatHistory = []; + this.isChatStarted = false; + } + + render() { + this.element.innerHTML = ` +
+ + + + +
+ + +
+ +
+ + +
+ +
+ + + + + `; + } + + initializeEventListeners() { + this.element.querySelector('.save-prompt').onclick = () => this.saveAsAgent(); + this.element.querySelector('.prompt-select').onchange = (e) => this.loadAgent(e.target.value); + this.element.querySelector('.settings-prompt').onclick = () => this.promptSettings.show(); + this.element.querySelector('.start-chat').onclick = () => this.startChat(); + this.element.querySelector('.run-prompt').onclick = () => this.handleRunPrompt(); + + // Add enter key support for chat input + this.element.querySelector('.user-prompt').addEventListener('keypress', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.handleRunPrompt(); + } + }); + } + + startChat() { + const userPrompt = this.element.querySelector('.user-prompt-template').value.trim(); + if (!userPrompt) { + alert('Please enter a user prompt to start the chat'); + return; + } + + // Show chat container + this.element.querySelector('.chat-container').style.display = 'block'; + + // Add AI response to initial prompt + this.addChatMessage('assistant', 'This is a simulated AI response.'); + + this.isChatStarted = true; + } + + handleRunPrompt() { + if (!this.isChatStarted) return; + + const userPrompt = this.element.querySelector('.user-prompt').value.trim(); + if (!userPrompt) return; + + this.addChatMessage('user', userPrompt); + this.addChatMessage('assistant', 'This is a simulated AI response.'); + + this.element.querySelector('.user-prompt').value = ''; + } + + addChatMessage(role, content) { + const messagesContainer = this.element.querySelector('.chat-messages'); + const messageElement = document.createElement('div'); + messageElement.className = `chat-message chat-message--${role}`; + messageElement.innerHTML = ` +
+ ${role === 'user' ? 'person' : 'smart_toy'} +
+
${content}
+ `; + messagesContainer.appendChild(messageElement); + + // Store in history + this.chatHistory.push({ role, content }); + + // Scroll to bottom + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + + async loadAgent(agentId) { + if (!agentId) return; + + const agent = this.agents.get(agentId); + if (agent) { + this.setContent(agent.prompt[0].content); + this.promptSettings.updateSettings(agent.config); + + this.element.dispatchEvent(new CustomEvent('prompt-updated', { + bubbles: true + })); + } + } + + async saveAsAgent() { + const name = this.getName(); + if (!name) return; + + const agentData = { + name, + prompt: [{ + role: 'system', + content: this.getContent() + }], + config: this.promptSettings.getSettings() + }; + + document.dispatchEvent(new CustomEvent('save-agent', { + detail: { agent: agentData } + })); + + this.showToast('Agent saved successfully'); + } + + getName() { + const name = this.element.querySelector('.agent-name').value.trim(); + if (!name) { + alert('Please enter an agent name'); + return null; + } + return name; + } + + getContent() { + return this.element.querySelector('.prompt-template').value; + } + + setContent(content) { + this.element.querySelector('.prompt-template').value = content; + } + + showToast(message) { + const toast = document.createElement('div'); + toast.className = 'toast toast--success'; + toast.textContent = message; + + document.body.appendChild(toast); + + // Trigger animation + setTimeout(() => toast.classList.add('toast--visible'), 100); + + // Remove after 3 seconds + setTimeout(() => { + toast.classList.remove('toast--visible'); + setTimeout(() => toast.remove(), 300); // Remove after fade animation + }, 3000); + } +} \ No newline at end of file diff --git a/src/static/js/components/promptSettings.js b/src/static/js/components/promptSettings.js new file mode 100644 index 0000000..b5e71e2 --- /dev/null +++ b/src/static/js/components/promptSettings.js @@ -0,0 +1,117 @@ +import { FormHelper } from '../utils/formHelper.js'; + +export class PromptSettings { + constructor() { + this.createModal(); + this.models = {}; + this.fetchModels(); + this.settings = { + model: 'gpt-4o', + temperature: 0.6, + max_tokens: 2000 + }; + } + + async fetchModels() { + try { + const response = await fetch('/api/config/models'); + this.models = await response.json(); + } catch (error) { + console.error('Failed to fetch models:', error); + } + } + + createModal() { + this.modal = document.createElement('div'); + this.modal.className = 'settings-popup'; + document.body.appendChild(this.modal); + } + + getSettings() { + return { ...this.settings }; + } + + updateSettings(newSettings) { + this.settings = { ...newSettings }; + } + + show() { + const modelOptions = Object.entries(this.models).flatMap(([provider, models]) => + models.map(model => ({ + value: model.id, + label: `${model.name} (${provider})` + })) + ); + + const fields = [ + { + label: 'Model', + type: 'select', + name: 'model', + options: modelOptions, + value: this.settings.model + }, + { + label: 'Temperature', + type: 'range', + name: 'temperature', + min: 0, + max: 1, + step: 0.1, + value: this.settings.temperature + }, + { + label: 'Max Tokens', + type: 'number', + name: 'max_tokens', + value: this.settings.max_tokens, + min: 1 + } + ]; + + this.modal.innerHTML = ` +
+

Prompt Settings

+
+
+ +
+
+ `; + + const form = this.modal.querySelector('.settings-form'); + fields.forEach(field => { + const formGroup = document.createElement('div'); + formGroup.className = 'form-group'; + + const label = document.createElement('label'); + label.className = 'form__label'; + label.textContent = field.label; + + const input = FormHelper.createInput(field); + + // Set initial value + input.value = this.settings[field.name]; + + const inputElement = field.type === 'range' ? input.querySelector('input') : input; + + inputElement.addEventListener('input', () => { + const newValue = field.type === 'range' ? parseFloat(inputElement.value) : + field.type === 'number' ? parseInt(inputElement.value) : + inputElement.value; + this.settings[field.name] = newValue; + }); + + formGroup.appendChild(label); + formGroup.appendChild(input); + form.appendChild(formGroup); + }); + + this.modal.style.display = 'flex'; + + // Close on outside click + this.modal.onclick = (e) => { + if (e.target === this.modal) this.modal.style.display = 'none'; + }; + } +} \ No newline at end of file From 8bda5292ed062612ccf42fe1c5c09236ecd95151 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 19:07:25 +0530 Subject: [PATCH 08/62] removed unused file --- server/app/firebase/dataset_dao.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 server/app/firebase/dataset_dao.py diff --git a/server/app/firebase/dataset_dao.py b/server/app/firebase/dataset_dao.py deleted file mode 100644 index e69de29..0000000 From 28cff96315a9376c5f20e0f3f0651b29e0b0fac3 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 19:18:41 +0530 Subject: [PATCH 09/62] minor refactor: renamed update_dataset_route to update_dataset --- server/app/api/dataset_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index 5bc8f7b..d078333 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -53,7 +53,7 @@ def create_dataset(): @dataset_bp.route("/", methods=["PUT"]) @login_required -def update_dataset_route(dataset_id): +def update_dataset(dataset_id): try: data = request.json dataset = Dataset.objects.get(id=dataset_id) From d58f8d73eb01bd1fa11966e50da7ac257246d0f3 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 19:19:50 +0530 Subject: [PATCH 10/62] renamed aiagent_routes.py to ai_agent_routes.py --- server/app/__init__.py | 2 +- server/app/api/{aiagent_routes.py => ai_agent_routes.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename server/app/api/{aiagent_routes.py => ai_agent_routes.py} (100%) diff --git a/server/app/__init__.py b/server/app/__init__.py index dc95f75..a4415f9 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -5,7 +5,7 @@ from flask import Flask from flask_cors import CORS -from .api.aiagent_routes import ai_agent_bp +from .api.ai_agent_routes import ai_agent_bp from .api.auth_routes import auth_bp from .api.config_routes import config_bp from .api.dataset_routes import dataset_bp diff --git a/server/app/api/aiagent_routes.py b/server/app/api/ai_agent_routes.py similarity index 100% rename from server/app/api/aiagent_routes.py rename to server/app/api/ai_agent_routes.py From b37cfdc5620c1973447d65be897f2f1549eb4643 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 19:37:41 +0530 Subject: [PATCH 11/62] added nested serializeer --- server/app/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/app/serializers.py b/server/app/serializers.py index a820ac8..1ac384f 100644 --- a/server/app/serializers.py +++ b/server/app/serializers.py @@ -1,4 +1,4 @@ -from marshmallow import Schema +from marshmallow import Schema, fields from server.app.models import * @@ -19,6 +19,8 @@ class Meta: class AIAgentVariantSerializer(Schema): + agent = fields.Nested(AIAgentSerializer()) + class Meta: model = AIAgentVariant From 023ebcbd7f1c9c98d104da5e93e6eb617c2294fa Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 20:04:43 +0530 Subject: [PATCH 12/62] bug fix --- server/app/api/ai_agent_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py index 685099c..b7afb29 100644 --- a/server/app/api/ai_agent_routes.py +++ b/server/app/api/ai_agent_routes.py @@ -107,7 +107,7 @@ def create_variant(agent_id): AIAgentVariant.objects.create( agent=agent, - variant_name=variant_name, + name=variant_name, variables=variables, is_alive=True, added_on=get_datetime_in_ist(), From 84515e0c1798cb5a8810b449a89d212bbf22cd03 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 20:35:25 +0530 Subject: [PATCH 13/62] using marshmallow-mongoengine, fixed serializers --- server/app/serializers.py | 12 ++++++------ server/requirements.txt | Bin 442 -> 424 bytes 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/app/serializers.py b/server/app/serializers.py index 1ac384f..43c288f 100644 --- a/server/app/serializers.py +++ b/server/app/serializers.py @@ -1,30 +1,30 @@ -from marshmallow import Schema, fields +from marshmallow_mongoengine import ModelSchema, fields from server.app.models import * -class UserSerializer(Schema): +class UserSerializer(ModelSchema): class Meta: model = User -class ProjectSerializer(Schema): +class ProjectSerializer(ModelSchema): class Meta: model = Project -class AIAgentSerializer(Schema): +class AIAgentSerializer(ModelSchema): class Meta: model = AIAgent -class AIAgentVariantSerializer(Schema): +class AIAgentVariantSerializer(ModelSchema): agent = fields.Nested(AIAgentSerializer()) class Meta: model = AIAgentVariant -class DatasetSerializer(Schema): +class DatasetSerializer(ModelSchema): class Meta: model = Dataset diff --git a/server/requirements.txt b/server/requirements.txt index 15c284dbbf3627340a5f32122d006a8252cd4a01..2ff323430f77ccc526cf271db5367af269057fa9 100644 GIT binary patch delta 72 zcmdnRyn=ay6yszU#vq$Sh9ZVyh75*WhD3%OAk1edXV3-m@)`0N(t%_un9c-}sSLJ2 PXuzPyV9a0$B#jsVV8{+L delta 62 zcmZ3%yo-5*6r*Y`Ln1>FLoq`J1BjIag!v5R47NaM%%I0$1SSn9i!z2!Zef(+Fk-L- IiW)Ki0HhcTr2qf` From 891f49036cf8bd8c4c9154b02be1c7eba94f9006 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 21:37:27 +0530 Subject: [PATCH 14/62] removed redundant index --- server/app/models.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/server/app/models.py b/server/app/models.py index 4a533ad..ab7faf5 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -23,15 +23,7 @@ class User(Document): added_on = DateTimeField() updated_on = DateTimeField() - meta = { - "collection": "users", - "indexes": [ - { - "fields": ["firebase_uid"], - "unique": True, - } - ], - } + meta = {"collection": "users"} class Project(Document): From 3de4bfa74391516681c17b29f76096e39c12ec1d Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 21:44:08 +0530 Subject: [PATCH 15/62] minor renamed: MULTITURN to MULTI_TURN --- server/app/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/enums.py b/server/app/enums.py index 9842d2e..3c32199 100644 --- a/server/app/enums.py +++ b/server/app/enums.py @@ -3,4 +3,4 @@ class DataSetTypes(Enum): KEY_VALUE = "KEY_VALUE" - MULTI_TURN = "MULTITURN" + MULTI_TURN = "MULTI_TURN" From b4ba177586457d75067e4ee1b78eba6193480519 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 22:10:31 +0530 Subject: [PATCH 16/62] added add, update, remove for samples in dataset --- server/app/api/dataset_routes.py | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index d078333..d58e26f 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -79,3 +79,67 @@ def remove_dataset(dataset_id): except Exception as e: logger.error(f"Error in deleting dataset: {str(e)}") return jsonify({"message": str(e)}), 500 + + +@dataset_bp.route("//samples", methods=["POST"]) +@login_required +def add_samples(dataset_id): + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + samples_set = set(dataset.samples) + samples_set.add(sample) + dataset.samples = list(samples_set) + dataset.updated_on = get_datetime_in_ist() + dataset.save() + logger.info(f"Samples added to dataset '{dataset_id}' successfully") + return jsonify({"message": f"Samples added to dataset '{dataset_id}' successfully"}) + except Exception as e: + logger.error(f"Error in adding samples to dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + +@dataset_bp.route("//samples", methods=["DELETE"]) +@login_required +def remove_samples(dataset_id): + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + samples_set = set(dataset.samples) + samples_set.remove(sample) + dataset.samples = list(samples_set) + dataset.updated_on = get_datetime_in_ist() + dataset.save() + logger.info(f"Samples removed from dataset '{dataset_id}' successfully") + return jsonify({"message": f"Samples removed from dataset '{dataset_id}' successfully"}) + except Exception as e: + logger.error(f"Error in removing samples from dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + +@dataset_bp.route("//samples", methods=["PUT"]) +@login_required +def update_sample(dataset_id): + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + samples_set = set(dataset.samples) + samples_set.remove(sample) + samples_set.add(data.get("updated_sample")) + dataset.samples = list(samples_set) + dataset.updated_on = get_datetime_in_ist() + dataset.save() + logger.info(f"Sample updated in dataset '{dataset_id}' successfully") + return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}) + except Exception as e: + logger.error(f"Error in updating sample in dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 From 7bf808e4c6c2bf75484c71aed513f442de781e55 Mon Sep 17 00:00:00 2001 From: kamal20122012 <38579759+kamal20122012@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:13:46 +0530 Subject: [PATCH 17/62] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/master_proback.yml | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/master_proback.yml diff --git a/.github/workflows/master_proback.yml b/.github/workflows/master_proback.yml new file mode 100644 index 0000000..43f58d6 --- /dev/null +++ b/.github/workflows/master_proback.yml @@ -0,0 +1,78 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions +# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions + +name: Build and deploy Python app to Azure Web App - proback + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python version + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + # Optional: Add step to run tests here (PyTest, Django test suites, etc.) + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment jobs + uses: actions/upload-artifact@v4 + with: + name: python-app + path: | + release.zip + !venv/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: python-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_03FB85159B9A4E78AACF5C9E49520FA7 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_CDBEEAED51A249279383A6E8687EC9C2 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_8376CC7B1D6F40DAACC0EE4E277694F7 }} + + - name: 'Deploy to Azure Web App' + uses: azure/webapps-deploy@v3 + id: deploy-to-webapp + with: + app-name: 'proback' + slot-name: 'Production' + \ No newline at end of file From eec2eae119946012c15c20ae1db9bedd10b76ca2 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 23:22:28 +0530 Subject: [PATCH 18/62] added name field in AIAgent and Dataset --- server/app/api/ai_agent_routes.py | 12 +++++++----- server/app/api/dataset_routes.py | 2 ++ server/app/models.py | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py index b7afb29..7af2750 100644 --- a/server/app/api/ai_agent_routes.py +++ b/server/app/api/ai_agent_routes.py @@ -46,6 +46,7 @@ def create_agent(): project = Project.objects.get(id=project_id) agent = AIAgent( project=project, + name=data["name"], agent_config=data["config"], prompt=data["prompt"], included_variables=data["included_variables"], @@ -67,9 +68,10 @@ def update_agent(agent_id): data = request.json agent_id = ObjectId(agent_id) agent = AIAgent.objects.get(id=agent_id) - agent.agent_config = data["config"] - agent.prompt = data["prompt"] - agent.included_variables = data["included_variables"] + agent.name = data.get("name", agent.name) + agent.agent_config = data.get("config", agent.agent_config) + agent.prompt = data.get("prompt", agent.prompt) + agent.included_variables = data.get("included_variables", agent.included_variables) agent.updated_on = get_datetime_in_ist() agent.save() logger.info("AI Agent updated successfully") @@ -98,8 +100,8 @@ def remove_agent(agent_id): def create_variant(agent_id): try: data = request.json - variant_name = data["variant_name"] - variables = data["variables"] + variant_name = data.get("name") + variables = data.get("variables") agent = AIAgent.objects.get(id=ObjectId(agent_id)) variable_names = variables.keys() if not set(variable_names).issubset(set(agent.included_variables)): diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index d58e26f..db8e4df 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -38,6 +38,7 @@ def create_dataset(): project = Project.objects.get(id=project_id) dataset = Dataset( project=project, + name=data["name"], type=data["type"], samples=data["samples"] or [], is_alive=True, @@ -59,6 +60,7 @@ def update_dataset(dataset_id): dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 + dataset.name = data.get("name", dataset.name) dataset.samples = data.get("samples", dataset.samples) dataset.updated_on = get_datetime_in_ist() dataset.save() diff --git a/server/app/models.py b/server/app/models.py index ab7faf5..63b294c 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -39,6 +39,7 @@ class Project(Document): class AIAgent(Document): project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) + name = StringField(required=True) agent_config = DictField(required=True) prompt = ListField(DictField(required=True)) included_variables = ListField(StringField()) @@ -62,6 +63,7 @@ class AIAgentVariant(Document): class Dataset(Document): project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) + name = StringField(required=True, min_length=1) type = StringField(required=True, choices=[item.value for item in DataSetTypes]) samples = ListField(DictField(), required=True) is_alive = BooleanField(default=True) From 420b32d40c2db3331bf2a094f807a337b79d0952 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 17 Nov 2024 23:23:34 +0530 Subject: [PATCH 19/62] removed aws creds check from yaml --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cb89dd..8c252be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,6 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: detect-aws-credentials - repo: https://github.com/PyCQA/isort rev: 5.13.2 From 0de4f91af72bdc152f2830e14d2dbef39ec6e046 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Tue, 19 Nov 2024 22:20:46 +0530 Subject: [PATCH 20/62] moved requirements in root folder --- server/requirements.txt => requirements.txt | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename server/requirements.txt => requirements.txt (100%) diff --git a/server/requirements.txt b/requirements.txt similarity index 100% rename from server/requirements.txt rename to requirements.txt From 228a7d5c9cf539a6ee377ab9656ea0be685a55e8 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Tue, 19 Nov 2024 22:36:11 +0530 Subject: [PATCH 21/62] adding firebase credentials --- firebase-credentials.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 firebase-credentials.json diff --git a/firebase-credentials.json b/firebase-credentials.json new file mode 100644 index 0000000..84f3bf9 --- /dev/null +++ b/firebase-credentials.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "proback-b0e41", + "private_key_id": "653740c05e978977411a4522bb4228c02a384ae3", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCRjHqnWsUtbisk\nf1Ttl4dKEThQjFz8shM9oMC/FKh0w9zukvSV5fW1cC2DQ+bkm4T/OqXadS9AiWSo\ns+CDACFauz+FbLMiM/o7giw4YViw7gMiBiQ3SsEsU2avSwEgLtW2FDNqNam+aqzV\nk6fvcsieu5gzcOofj5qZiOYLWYXvbPm79CmN07Yd9ziJBEkV/bDwPqaGunzaaHQV\nAp0QskkHOWVQNR//W91B5uR3oRigJHmRoKpycsrmwD/znC+dl2nzoA4aq1cg3Vbs\n3B4sac7fe3EUOHTEzLh5FJNtfVeeaNzjMm7tKGeYQag54MJNlPLvZFzZtmkMcJwQ\nxEmX+TQtAgMBAAECggEAG1q4jo1rs28aIqDfuEubPJth5xmKPF/HZ/S3ZPIf+tn4\nwsnRt/5hIHl1xE7VrS9hXoqKMzWE7O8lgONVlalHm4LHnG7id9Im/Fopr2r0PURu\nSE25Lr8Y8Ri3mshQ7NiAh9jiFNsDDOuFkhpPHJyJAfQl1b0p80aM+sAU5BsuJeIf\nmoWhcNJ7uXzmuJomKZf8sLQKj40KiMUAH8YhaCbppnGJ198D2K+SVgLp6rl6yi0q\nlCVQ9hKuK47WC+c+wR8Hm4nbwVbOdZECd4U3X+FYzg5timkYI3TohEw7khZnzyDY\nfCHRHgAtwoIpXieQASHVCyg8UQgRwpZci4KK4qpmgQKBgQDMwO25uYrnR+OupXtC\nayCDk1FuC35xtJW7h9JOp5V9yRuNinMUHj3HEnFeIT3IQCcm59Ts+t3qFQwDosge\n5EcpnncEOaL5eRDoJlqfsIlWGao0PbbYKaGBN47or8f+z+t8AK1dCE1vg8qLdlmc\nM+HtL6HKiLzmF4tdZWCkv2luTQKBgQC1+iW3y7kNcGhK5ueuvAdXtP7HIeQlIcz3\nQyuq6mJSSp/yTjWyLj1DO8y8siRuSmqXSVaTrsAgD47VBfUT8BWlL90WMzO53hkg\nQx07nm0Vf6UPAw36NQNaJgJK0J9hS9jRCYvhyDqvh0zRpI+QKz6gd6yiYINmGCn0\nc5Tk+16NYQKBgA6KTNd5k6Gk+0g1vDEVRnPRjGDSNCKC/ncYyBiJ0hXiqDA++rkd\nBKoUZzSWC7siwyUCJZ7Gmee7ouTHNIrtDxinhjAon6gzrr2tq5XXhYk1vV6EW22y\nVyPD/EQN55fyz/g4XBdVNZqLs2CAAREUpiCf1094smFsnpr9TgWlCimpAoGAEBAx\nriSshBQtEJeLUfBFEafnaXqDYge2yQjD3QVtgmgV8FSZBs5wF/Q6YVm0P4wQFqSh\noM2nJe1ZpVIuTQE/V+J+uIvyf/Cr9R4XgYC9jk1DK60QWcq+LgH259W8i+EfFWyr\nTVXYYFAoJsVI6uf2UkHeGkM8thFh+rMNLJ1HvmECgYBXTAOhli34cwp4YHiIsmHj\nQ/Zi96Q/iO7aJ77s+DtAZv5L8LczpmCIYpE7jMPfuWuWErTQMmwkFNnak43uJZFD\nyAjMDpiChxTVuldYJnhPXPmnMaVMvH09J0vKpvs/Qjli+c6by+o1wYzV/HpbqKHr\nymi3srFJstORq0iyLixHqA==\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-p1gi3@proback-b0e41.iam.gserviceaccount.com", + "client_id": "114680619399366509427", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-p1gi3%40proback-b0e41.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + } From 85e2025f0b00257583382d60f01527e58c3e3a8d Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Thu, 21 Nov 2024 00:52:49 +0530 Subject: [PATCH 22/62] import fix --- server/app/core/services/ai_agent_services.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/app/core/services/ai_agent_services.py b/server/app/core/services/ai_agent_services.py index 5d1672c..5baae01 100644 --- a/server/app/core/services/ai_agent_services.py +++ b/server/app/core/services/ai_agent_services.py @@ -1,7 +1,5 @@ -import logging - -from server.app.config import Config, Logging -from server.app.core.services.llm_response_service import LLMResponseService +from ...config import Config, Logging +from ..services.llm_response_service import LLMResponseService logger = Logging.get_logger(__name__) From 7b5dd12e1e7665f768ad01c82a072a532aad16db Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Thu, 21 Nov 2024 00:53:47 +0530 Subject: [PATCH 23/62] deleted firebase-credentials.json --- firebase-credentials.json | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 firebase-credentials.json diff --git a/firebase-credentials.json b/firebase-credentials.json deleted file mode 100644 index 84f3bf9..0000000 --- a/firebase-credentials.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "proback-b0e41", - "private_key_id": "653740c05e978977411a4522bb4228c02a384ae3", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCRjHqnWsUtbisk\nf1Ttl4dKEThQjFz8shM9oMC/FKh0w9zukvSV5fW1cC2DQ+bkm4T/OqXadS9AiWSo\ns+CDACFauz+FbLMiM/o7giw4YViw7gMiBiQ3SsEsU2avSwEgLtW2FDNqNam+aqzV\nk6fvcsieu5gzcOofj5qZiOYLWYXvbPm79CmN07Yd9ziJBEkV/bDwPqaGunzaaHQV\nAp0QskkHOWVQNR//W91B5uR3oRigJHmRoKpycsrmwD/znC+dl2nzoA4aq1cg3Vbs\n3B4sac7fe3EUOHTEzLh5FJNtfVeeaNzjMm7tKGeYQag54MJNlPLvZFzZtmkMcJwQ\nxEmX+TQtAgMBAAECggEAG1q4jo1rs28aIqDfuEubPJth5xmKPF/HZ/S3ZPIf+tn4\nwsnRt/5hIHl1xE7VrS9hXoqKMzWE7O8lgONVlalHm4LHnG7id9Im/Fopr2r0PURu\nSE25Lr8Y8Ri3mshQ7NiAh9jiFNsDDOuFkhpPHJyJAfQl1b0p80aM+sAU5BsuJeIf\nmoWhcNJ7uXzmuJomKZf8sLQKj40KiMUAH8YhaCbppnGJ198D2K+SVgLp6rl6yi0q\nlCVQ9hKuK47WC+c+wR8Hm4nbwVbOdZECd4U3X+FYzg5timkYI3TohEw7khZnzyDY\nfCHRHgAtwoIpXieQASHVCyg8UQgRwpZci4KK4qpmgQKBgQDMwO25uYrnR+OupXtC\nayCDk1FuC35xtJW7h9JOp5V9yRuNinMUHj3HEnFeIT3IQCcm59Ts+t3qFQwDosge\n5EcpnncEOaL5eRDoJlqfsIlWGao0PbbYKaGBN47or8f+z+t8AK1dCE1vg8qLdlmc\nM+HtL6HKiLzmF4tdZWCkv2luTQKBgQC1+iW3y7kNcGhK5ueuvAdXtP7HIeQlIcz3\nQyuq6mJSSp/yTjWyLj1DO8y8siRuSmqXSVaTrsAgD47VBfUT8BWlL90WMzO53hkg\nQx07nm0Vf6UPAw36NQNaJgJK0J9hS9jRCYvhyDqvh0zRpI+QKz6gd6yiYINmGCn0\nc5Tk+16NYQKBgA6KTNd5k6Gk+0g1vDEVRnPRjGDSNCKC/ncYyBiJ0hXiqDA++rkd\nBKoUZzSWC7siwyUCJZ7Gmee7ouTHNIrtDxinhjAon6gzrr2tq5XXhYk1vV6EW22y\nVyPD/EQN55fyz/g4XBdVNZqLs2CAAREUpiCf1094smFsnpr9TgWlCimpAoGAEBAx\nriSshBQtEJeLUfBFEafnaXqDYge2yQjD3QVtgmgV8FSZBs5wF/Q6YVm0P4wQFqSh\noM2nJe1ZpVIuTQE/V+J+uIvyf/Cr9R4XgYC9jk1DK60QWcq+LgH259W8i+EfFWyr\nTVXYYFAoJsVI6uf2UkHeGkM8thFh+rMNLJ1HvmECgYBXTAOhli34cwp4YHiIsmHj\nQ/Zi96Q/iO7aJ77s+DtAZv5L8LczpmCIYpE7jMPfuWuWErTQMmwkFNnak43uJZFD\nyAjMDpiChxTVuldYJnhPXPmnMaVMvH09J0vKpvs/Qjli+c6by+o1wYzV/HpbqKHr\nymi3srFJstORq0iyLixHqA==\n-----END PRIVATE KEY-----\n", - "client_email": "firebase-adminsdk-p1gi3@proback-b0e41.iam.gserviceaccount.com", - "client_id": "114680619399366509427", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-p1gi3%40proback-b0e41.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" - } From 0d2233f5d3ae315c716f16a659e840b91928ce55 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Thu, 21 Nov 2024 00:59:24 +0530 Subject: [PATCH 24/62] fixed imports --- server/app/core/services/llm_response_service.py | 6 +++--- server/app/models.py | 2 +- server/app/serializers.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/app/core/services/llm_response_service.py b/server/app/core/services/llm_response_service.py index 937ed41..cd06356 100644 --- a/server/app/core/services/llm_response_service.py +++ b/server/app/core/services/llm_response_service.py @@ -1,6 +1,6 @@ -from server.app.config import Config -from server.app.utils import get_response_claude, get_response_openai -from server.app.utils.model_map import get_model_type +from ...config import Config +from ...utils import get_response_claude, get_response_openai +from ...utils.model_map import get_model_type response_function_map = {"OpenAI": get_response_openai, "Anthropic": get_response_claude} diff --git a/server/app/models.py b/server/app/models.py index 63b294c..4f37e97 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -11,7 +11,7 @@ StringField, ) -from server.app.enums import DataSetTypes +from .enums import DataSetTypes class User(Document): diff --git a/server/app/serializers.py b/server/app/serializers.py index 43c288f..2ae5a0f 100644 --- a/server/app/serializers.py +++ b/server/app/serializers.py @@ -1,6 +1,6 @@ from marshmallow_mongoengine import ModelSchema, fields -from server.app.models import * +from .models import * class UserSerializer(ModelSchema): From 61310405aa22a99e9728749836c58de3db231aa1 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Thu, 21 Nov 2024 01:48:35 +0530 Subject: [PATCH 25/62] renames app.py to run.py --- server/{app.py => run.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/{app.py => run.py} (100%) diff --git a/server/app.py b/server/run.py similarity index 100% rename from server/app.py rename to server/run.py From 2bc2d9bc4714ce5395a2796ccb0697b46c198fb7 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 01:52:03 +0530 Subject: [PATCH 26/62] modified the data structure to allow for nested data also added base-model to add repeated fields automatically --- server/app/api/ai_agent_routes.py | 101 +++++++++++++++--------------- server/app/api/auth_routes.py | 2 - server/app/api/dataset_routes.py | 62 +++++++++--------- server/app/api/project_routes.py | 20 +++--- server/app/core/datasets.py | 0 server/app/core/models.py | 0 server/app/models.py | 73 ++++++++++++--------- server/app/serializers.py | 12 ++-- 8 files changed, 139 insertions(+), 131 deletions(-) delete mode 100644 server/app/core/datasets.py delete mode 100644 server/app/core/models.py diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py index 7af2750..5532d9d 100644 --- a/server/app/api/ai_agent_routes.py +++ b/server/app/api/ai_agent_routes.py @@ -4,7 +4,7 @@ from ..auth.decorators import login_required from ..config import Logging from ..core.services.ai_agent_services import generate_conversation_service -from ..models import AIAgent, AIAgentVariant, Project +from ..models import Project from ..serializers import AIAgentSerializer, AIAgentVariantSerializer from ..utils.datetime_utils import get_datetime_in_ist @@ -13,14 +13,13 @@ ai_agent_bp = Blueprint("ai_agents", __name__) -@ai_agent_bp.route("/", methods=["GET"]) +@ai_agent_bp.route("//agents", methods=["GET"]) @login_required def get_agents(project_id): try: project_id = ObjectId(project_id) project = Project.objects.get(id=project_id) - ai_agents = AIAgent.objects.filter(project=project, is_alive=True) - serialized_agents = AIAgentSerializer(many=True).dump(ai_agents) + serialized_agents = AIAgentSerializer(many=True).dump(project.ai_agents) logger.info(f"Successfully fetched ai agents for the project: {str(project_id)}") return ( jsonify( @@ -36,44 +35,40 @@ def get_agents(project_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("/", methods=["POST"]) +@ai_agent_bp.route("//agents", methods=["POST"]) @login_required -def create_agent(): +def create_agent(project_id): try: data = request.json - project_id_unsafe = data.get("project_id") - project_id = ObjectId(project_id_unsafe) - project = Project.objects.get(id=project_id) - agent = AIAgent( - project=project, - name=data["name"], - agent_config=data["config"], - prompt=data["prompt"], - included_variables=data["included_variables"], - is_alive=True, - added_on=get_datetime_in_ist(), - updated_on=get_datetime_in_ist(), - ).save() + project = Project.objects.get(id=ObjectId(project_id)) + new_agent = { + "name": data["name"], + "agent_config": data["config"], + "prompt": data["prompt"], + "included_variables": data["included_variables"], + "is_alive": True, + } + project.ai_agents.append(new_agent) + project.save() logger.info("AI Agent created successfully") - return jsonify({"message": "AI Agent created successfully", "id": str(agent.id)}), 201 + return jsonify({"message": "AI Agent created successfully"}), 201 except Exception as e: logger.error(f"Error in creating AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("/", methods=["PUT"]) +@ai_agent_bp.route("//agents/", methods=["PUT"]) @login_required -def update_agent(agent_id): +def update_agent(project_id, agent_index): try: data = request.json - agent_id = ObjectId(agent_id) - agent = AIAgent.objects.get(id=agent_id) + project = Project.objects.get(id=ObjectId(project_id)) + agent = project.ai_agents[int(agent_index)] agent.name = data.get("name", agent.name) agent.agent_config = data.get("config", agent.agent_config) agent.prompt = data.get("prompt", agent.prompt) agent.included_variables = data.get("included_variables", agent.included_variables) - agent.updated_on = get_datetime_in_ist() - agent.save() + project.save() logger.info("AI Agent updated successfully") return jsonify({"message": "AI Agent updated successfully"}), 200 except Exception as e: @@ -81,13 +76,13 @@ def update_agent(agent_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("/", methods=["DELETE"]) +@ai_agent_bp.route("//agents/", methods=["DELETE"]) @login_required -def remove_agent(agent_id): +def remove_agent(project_id, agent_index): try: - agent_id = ObjectId(agent_id) - agent = AIAgent.objects.get(id=agent_id) - agent.delete() + project = Project.objects.get(id=ObjectId(project_id)) + del project.ai_agents[int(agent_index)] + project.save() logger.info("AI Agent deleted successfully!") return jsonify({"message": "AI Agent deleted successfully!"}), 200 except Exception as e: @@ -95,26 +90,28 @@ def remove_agent(agent_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//variants", methods=["POST"]) +@ai_agent_bp.route("//agents//variants", methods=["POST"]) @login_required -def create_variant(agent_id): +def create_variant(project_id, agent_index): try: data = request.json variant_name = data.get("name") variables = data.get("variables") - agent = AIAgent.objects.get(id=ObjectId(agent_id)) + + project = Project.objects.get(id=ObjectId(project_id)) + agent = project.ai_agents[int(agent_index)] + variable_names = variables.keys() if not set(variable_names).issubset(set(agent.included_variables)): return jsonify({"error": "Variables not included in the agent"}), 400 - AIAgentVariant.objects.create( - agent=agent, - name=variant_name, - variables=variables, - is_alive=True, - added_on=get_datetime_in_ist(), - updated_on=get_datetime_in_ist(), - ) + new_variant = { + "name": variant_name, + "variables": variables, + "is_alive": True, + } + agent.variants.append(new_variant) + project.save() logger.info("Variant created successfully") return jsonify({"message": "Variant created successfully"}), 201 except Exception as e: @@ -122,13 +119,13 @@ def create_variant(agent_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//variants", methods=["GET"]) +@ai_agent_bp.route("//agents//variants", methods=["GET"]) @login_required -def get_variants(agent_id): +def get_variants(project_id, agent_index): try: - agent = AIAgent.objects.get(id=ObjectId(agent_id)) - variants = AIAgentVariant.objects.filter(agent=agent, is_alive=True) - serialized_variants = AIAgentVariantSerializer(many=True).dump(variants) + project = Project.objects.get(id=ObjectId(project_id)) + agent = project.ai_agents[int(agent_index)] + serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) logger.info("Successfully fetched variants") return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 except Exception as e: @@ -136,12 +133,14 @@ def get_variants(agent_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("/variants/", methods=["DELETE"]) +@ai_agent_bp.route("//agents//variants/", methods=["DELETE"]) @login_required -def remove_variant(variant_id): +def remove_variant(project_id, agent_index, variant_index): try: - variant = AIAgentVariant.objects.get(variant_id=ObjectId(variant_id)) - variant.delete() + project = Project.objects.get(id=ObjectId(project_id)) + agent = project.ai_agents[int(agent_index)] + del agent.variants[int(variant_index)] + project.save() logger.info("Variant deleted successfully!") return jsonify({"message": "Variant deleted successfully!"}), 200 except Exception as e: diff --git a/server/app/api/auth_routes.py b/server/app/api/auth_routes.py index 3e5f048..b69662d 100644 --- a/server/app/api/auth_routes.py +++ b/server/app/api/auth_routes.py @@ -32,8 +32,6 @@ def verify_token(): email=decoded_token.get("email"), picture=decoded_token.get("picture"), firebase_uid=decoded_token.get("uid"), - added_on=get_datetime_in_ist(), - updated_on=get_datetime_in_ist(), is_alive=True, ) user.save() diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py index db8e4df..181f34f 100644 --- a/server/app/api/dataset_routes.py +++ b/server/app/api/dataset_routes.py @@ -19,9 +19,7 @@ def get_datasets(project_id): project_id = ObjectId(project_id) project = Project.objects.get(id=project_id) datasets = Dataset.objects.filter(project=project, is_alive=True) - serialized_datasets = DatasetSerializer( - many=True, - ).dump(datasets) + serialized_datasets = DatasetSerializer(many=True).dump(datasets) logger.info(f"Successfully fetched datasets for the project: {str(project_id)}") return jsonify({"message": "Successfully fetched datasets", "data": serialized_datasets}) except Exception as e: @@ -40,10 +38,8 @@ def create_dataset(): project=project, name=data["name"], type=data["type"], - samples=data["samples"] or [], + samples=data.get("samples", []), is_alive=True, - added_on=get_datetime_in_ist(), - updated_on=get_datetime_in_ist(), ).save() logger.info(f"Dataset '{data['name']}' created successfully") return jsonify({"message": f"Dataset '{data['name']}' created successfully", "id": str(dataset.id)}), 201 @@ -62,10 +58,9 @@ def update_dataset(dataset_id): return jsonify({"message": "Dataset not found"}), 404 dataset.name = data.get("name", dataset.name) dataset.samples = data.get("samples", dataset.samples) - dataset.updated_on = get_datetime_in_ist() dataset.save() - logger.info(f"Dataset '{data['name']}' updated successfully") - return jsonify({"message": f"Dataset '{data['name']}' updated successfully"}) + logger.info(f"Dataset '{data.get('name', dataset.name)}' updated successfully") + return jsonify({"message": f"Dataset '{data.get('name', dataset.name)}' updated successfully"}), 200 except Exception as e: logger.error(f"Error in updating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 @@ -75,8 +70,11 @@ def update_dataset(dataset_id): @login_required def remove_dataset(dataset_id): try: - Dataset.objects.get(id=dataset_id).delete() - logger.info("Dataset deleted successfully!") + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + dataset.delete() + logger.info(f"Dataset deleted successfully! with id: {dataset_id}") return jsonify({"message": "Dataset deleted successfully! with id: " + dataset_id}), 200 except Exception as e: logger.error(f"Error in deleting dataset: {str(e)}") @@ -92,15 +90,14 @@ def add_samples(dataset_id): if not dataset: return jsonify({"message": "Dataset not found"}), 404 sample = data.get("sample") or {} - samples_set = set(dataset.samples) - samples_set.add(sample) - dataset.samples = list(samples_set) - dataset.updated_on = get_datetime_in_ist() + if sample in dataset.samples: + return jsonify({"message": "Sample already exists in the dataset"}), 400 + dataset.samples.append(sample) dataset.save() - logger.info(f"Samples added to dataset '{dataset_id}' successfully") - return jsonify({"message": f"Samples added to dataset '{dataset_id}' successfully"}) + logger.info(f"Sample added to dataset '{dataset_id}' successfully") + return jsonify({"message": f"Sample added to dataset '{dataset_id}' successfully"}), 200 except Exception as e: - logger.error(f"Error in adding samples to dataset: {str(e)}") + logger.error(f"Error in adding sample to dataset: {str(e)}") return jsonify({"message": str(e)}), 500 @@ -113,15 +110,15 @@ def remove_samples(dataset_id): if not dataset: return jsonify({"message": "Dataset not found"}), 404 sample = data.get("sample") or {} - samples_set = set(dataset.samples) - samples_set.remove(sample) - dataset.samples = list(samples_set) - dataset.updated_on = get_datetime_in_ist() - dataset.save() - logger.info(f"Samples removed from dataset '{dataset_id}' successfully") - return jsonify({"message": f"Samples removed from dataset '{dataset_id}' successfully"}) + if sample in dataset.samples: + dataset.samples.remove(sample) + dataset.save() + logger.info(f"Sample removed from dataset '{dataset_id}' successfully") + return jsonify({"message": f"Sample removed from dataset '{dataset_id}' successfully"}), 200 + else: + return jsonify({"message": "Sample not found in dataset"}), 404 except Exception as e: - logger.error(f"Error in removing samples from dataset: {str(e)}") + logger.error(f"Error in removing sample from dataset: {str(e)}") return jsonify({"message": str(e)}), 500 @@ -134,14 +131,15 @@ def update_sample(dataset_id): if not dataset: return jsonify({"message": "Dataset not found"}), 404 sample = data.get("sample") or {} - samples_set = set(dataset.samples) - samples_set.remove(sample) - samples_set.add(data.get("updated_sample")) - dataset.samples = list(samples_set) - dataset.updated_on = get_datetime_in_ist() + updated_sample = data.get("updated_sample") or {} + try: + index = dataset.samples.index(sample) + except ValueError: + return jsonify({"message": "Sample not found in dataset"}), 404 + dataset.samples[index] = updated_sample dataset.save() logger.info(f"Sample updated in dataset '{dataset_id}' successfully") - return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}) + return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}), 200 except Exception as e: logger.error(f"Error in updating sample in dataset: {str(e)}") return jsonify({"message": str(e)}), 500 diff --git a/server/app/api/project_routes.py b/server/app/api/project_routes.py index f846c66..6f8f019 100644 --- a/server/app/api/project_routes.py +++ b/server/app/api/project_routes.py @@ -61,14 +61,13 @@ def create_project(): project = Project( user=user, - name=data["name"] or "Default Project", - description=data["description"] or "", - added_on=get_datetime_in_ist(), - updated_on=get_datetime_in_ist(), + name=data.get("name", "Default Project"), + description=data.get("description", ""), + ai_agents=[], # Initialize empty list for embedded agents is_alive=True, ).save() - logger.info(f"Project '{data['name']}' created successfully") - return jsonify({"message": f"Project '{data['name']}' created successfully", "id": str(project.id)}), 201 + logger.info(f"Project '{project.name}' created successfully") + return jsonify({"message": f"Project '{project.name}' created successfully", "id": str(project.id)}), 201 except Exception as e: logger.error(f"Error in creating project: {str(e)}") return jsonify({"error": str(e)}), 500 @@ -81,12 +80,11 @@ def update_project(project_id): data = request.json project_id = ObjectId(project_id) project = Project.objects.get(id=project_id) - project.name = data["name"] - project.description = data["description"] - project.updated_on = get_datetime_in_ist() + project.name = data.get("name", project.name) + project.description = data.get("description", project.description) project.save() - logger.info(f"Project '{data['name']}' updated successfully") - return jsonify({"message": f"Project '{data['name']}' updated successfully"}), 200 + logger.info(f"Project '{project.name}' updated successfully") + return jsonify({"message": f"Project '{project.name}' updated successfully"}), 200 except Exception as e: logger.error(f"Error in updating project: {str(e)}") return jsonify({"error": str(e)}), 500 diff --git a/server/app/core/datasets.py b/server/app/core/datasets.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/app/core/models.py b/server/app/core/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/app/models.py b/server/app/models.py index 4f37e97..cd27135 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -6,68 +6,81 @@ DictField, Document, EmailField, + EmbeddedDocument, + EmbeddedDocumentField, ListField, ReferenceField, StringField, ) +from server.app.utils.datetime_utils import get_datetime_in_ist + from .enums import DataSetTypes -class User(Document): +class BaseDocument(Document): + is_alive = BooleanField(default=True) + added_on = DateTimeField(default=get_datetime_in_ist()) + updated_on = DateTimeField(default=get_datetime_in_ist()) + + meta = {"abstract": True} + + def save(self, *args, **kwargs): + if not self.added_on: + self.added_on = get_datetime_in_ist() + self.updated_on = get_datetime_in_ist() + return super(BaseDocument, self).save(*args, **kwargs) + + +class BaseEmbeddedDocument(EmbeddedDocument): + is_alive = BooleanField(default=True) + added_on = DateTimeField(default=get_datetime_in_ist()) + updated_on = DateTimeField(default=get_datetime_in_ist()) + + def save(self, *args, **kwargs): + if not self.added_on: + self.added_on = get_datetime_in_ist() + self.updated_on = get_datetime_in_ist() + return super(BaseEmbeddedDocument, self).save(*args, **kwargs) + + +class User(BaseDocument): name = StringField(required=True, min_length=1) firebase_uid = StringField(required=True, unique=True) picture = StringField() email = EmailField(required=True, unique=True) - is_alive = BooleanField(default=True) - added_on = DateTimeField() - updated_on = DateTimeField() meta = {"collection": "users"} -class Project(Document): - name = StringField(required=True, min_length=1) - user = ReferenceField(User, required=True, reverse_delete_rule=DENY) - description = StringField() - is_alive = BooleanField(default=True) - added_on = DateTimeField() - updated_on = DateTimeField() - - meta = {"collection": "projects"} +class AIAgentVariant(BaseEmbeddedDocument): + name = StringField(required=True) + variables = DictField(required=True) -class AIAgent(Document): - project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) +class AIAgent(BaseDocument): name = StringField(required=True) agent_config = DictField(required=True) prompt = ListField(DictField(required=True)) included_variables = ListField(StringField()) - is_alive = BooleanField(default=True) - added_on = DateTimeField() - updated_on = DateTimeField() + variants = ListField(EmbeddedDocumentField(AIAgentVariant)) meta = {"collection": "ai_agents"} -class AIAgentVariant(Document): - agent = ReferenceField(AIAgent, required=True, reverse_delete_rule=DENY) - name = StringField(required=True) - variables = DictField(required=True) - is_alive = BooleanField(default=True) - added_on = DateTimeField() - updated_on = DateTimeField() +class Project(BaseDocument): + name = StringField(required=True, min_length=1) + user = ReferenceField(User, required=True, reverse_delete_rule=DENY) + description = StringField() + ai_agents = ListField(EmbeddedDocumentField(AIAgent)) - meta = {"collection": "ai_agent_variants"} + meta = {"collection": "projects"} -class Dataset(Document): +class Dataset(BaseDocument): project = ReferenceField(Project, required=True, reverse_delete_rule=DENY) name = StringField(required=True, min_length=1) type = StringField(required=True, choices=[item.value for item in DataSetTypes]) samples = ListField(DictField(), required=True) - is_alive = BooleanField(default=True) - added_on = DateTimeField() - updated_on = DateTimeField() meta = {"collection": "datasets"} diff --git a/server/app/serializers.py b/server/app/serializers.py index 2ae5a0f..568c789 100644 --- a/server/app/serializers.py +++ b/server/app/serializers.py @@ -8,21 +8,23 @@ class Meta: model = User -class ProjectSerializer(ModelSchema): +class AIAgentVariantSerializer(ModelSchema): class Meta: - model = Project + model = AIAgentVariant class AIAgentSerializer(ModelSchema): + variants = fields.Nested(AIAgentVariantSerializer, many=True) + class Meta: model = AIAgent -class AIAgentVariantSerializer(ModelSchema): - agent = fields.Nested(AIAgentSerializer()) +class ProjectSerializer(ModelSchema): + ai_agents = fields.Nested(AIAgentSerializer, many=True) class Meta: - model = AIAgentVariant + model = Project class DatasetSerializer(ModelSchema): From 4fb0f106b3de0b2add2202d058fe995b63336921 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 02:15:29 +0530 Subject: [PATCH 27/62] added fixes in the code --- .gitignore | 3 ++- server/app/__init__.py | 16 ++++++++++------ server/app/models.py | 4 +++- server/run.py | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 3b75b79..48a83d4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ htmlcov/ .tox/ docs/_build/ *.json -firebase-credentials.json \ No newline at end of file +firebase-credentials.json +*.log diff --git a/server/app/__init__.py b/server/app/__init__.py index a4415f9..b9f37ee 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -17,6 +17,15 @@ Logging.initialize(log_level=logging.INFO, log_file="application.log") +def register_blueprints(app): + app.register_blueprint(main_bp) + app.register_blueprint(projects_bp, url_prefix="/api/projects") + app.register_blueprint(ai_agent_bp, url_prefix="/api/ai_agents") + app.register_blueprint(dataset_bp, url_prefix="/api/datasets") + app.register_blueprint(config_bp, url_prefix="/api/config") + app.register_blueprint(auth_bp, url_prefix="/api/auth") + + def create_app(config_class=Config): app = Flask(__name__, static_folder="../../src/static", template_folder="../../src/templates") @@ -33,11 +42,6 @@ def create_app(config_class=Config): # Initialize CORS CORS(app) - app.register_blueprint(main_bp) - app.register_blueprint(projects_bp, url_prefix="/api/projects") - app.register_blueprint(ai_agent_bp, url_prefix="/api/ai_agents") - app.register_blueprint(dataset_bp, url_prefix="/api/datasets") - app.register_blueprint(config_bp, url_prefix="/api/config") - app.register_blueprint(auth_bp, url_prefix="/api/auth") + register_blueprints(app) return app diff --git a/server/app/models.py b/server/app/models.py index cd27135..6958cee 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -37,6 +37,8 @@ class BaseEmbeddedDocument(EmbeddedDocument): added_on = DateTimeField(default=get_datetime_in_ist()) updated_on = DateTimeField(default=get_datetime_in_ist()) + meta = {"allow_inheritance": True} + def save(self, *args, **kwargs): if not self.added_on: self.added_on = get_datetime_in_ist() @@ -58,7 +60,7 @@ class AIAgentVariant(BaseEmbeddedDocument): variables = DictField(required=True) -class AIAgent(BaseDocument): +class AIAgent(BaseEmbeddedDocument): name = StringField(required=True) agent_config = DictField(required=True) prompt = ListField(DictField(required=True)) diff --git a/server/run.py b/server/run.py index 488dae9..83f8581 100644 --- a/server/run.py +++ b/server/run.py @@ -1,4 +1,4 @@ -from app import create_app +from server.app import create_app app = create_app() From b585ed985dd0dcbadb1c387228429484397a5515 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 02:59:57 +0530 Subject: [PATCH 28/62] added more fixes --- server/app/__init__.py | 1 + server/app/api/ai_agent_routes.py | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index b9f37ee..c1ea571 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -42,6 +42,7 @@ def create_app(config_class=Config): # Initialize CORS CORS(app) + # Register blueprints register_blueprints(app) return app diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py index 5532d9d..5ed56fb 100644 --- a/server/app/api/ai_agent_routes.py +++ b/server/app/api/ai_agent_routes.py @@ -4,9 +4,8 @@ from ..auth.decorators import login_required from ..config import Logging from ..core.services.ai_agent_services import generate_conversation_service -from ..models import Project +from ..models import AIAgent, AIAgentVariant, Project from ..serializers import AIAgentSerializer, AIAgentVariantSerializer -from ..utils.datetime_utils import get_datetime_in_ist logger = Logging.get_logger(__name__) @@ -41,13 +40,16 @@ def create_agent(project_id): try: data = request.json project = Project.objects.get(id=ObjectId(project_id)) - new_agent = { - "name": data["name"], - "agent_config": data["config"], - "prompt": data["prompt"], - "included_variables": data["included_variables"], - "is_alive": True, - } + + # Create an AIAgent instance + new_agent = AIAgent( + name=data["name"], + agent_config=data["config"], + prompt=data["prompt"], + included_variables=data["included_variables"], + is_alive=True, + ) + project.ai_agents.append(new_agent) project.save() logger.info("AI Agent created successfully") @@ -105,11 +107,8 @@ def create_variant(project_id, agent_index): if not set(variable_names).issubset(set(agent.included_variables)): return jsonify({"error": "Variables not included in the agent"}), 400 - new_variant = { - "name": variant_name, - "variables": variables, - "is_alive": True, - } + # Create an AIAgentVariant instance + new_variant = AIAgentVariant(name=variant_name, variables=variables, is_alive=True) agent.variants.append(new_variant) project.save() logger.info("Variant created successfully") From 125a21e4466f3b73e219d1e6b2e6dae9c17b8e97 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 03:34:06 +0530 Subject: [PATCH 29/62] added evaluation service and added lint ignore to f"" with no placeholders --- .flake8 | 2 +- requirements.txt | Bin 424 -> 492 bytes .../app/core/services/evaluation_services.py | 131 ++++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 server/app/core/services/evaluation_services.py diff --git a/.flake8 b/.flake8 index 99f72c4..5269548 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] -extend-ignore = E203, E266, E501, W503, F403, F401, F841, F405 +extend-ignore = E203, E266, E501, W503, F403, F401, F841, F405, F541 max-line-length = 120 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ff323430f77ccc526cf271db5367af269057fa9..40f52e6d16731051f4471adbbeeb677e22747890 100644 GIT binary patch delta 91 zcmZ3%{Dygg#$+i*jfols%4rOx3{^l_$xsew*)rHN7%=ED7&2G@NrTC$jLy=$3|tI( cKoz+R1wgeR6-GcgFrGMBeX9Yv$!?5FlP567OjOaCoW`gz@r4}#j8h5P diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py new file mode 100644 index 0000000..e82d1dd --- /dev/null +++ b/server/app/core/services/evaluation_services.py @@ -0,0 +1,131 @@ +from abc import ABC, abstractmethod + +import numpy as np +from fuzzywuzzy import fuzz + +from server.app.core.services.llm_response_service import LLMResponseService + +from ...config import Logging + +logger = Logging.get_logger(__name__) + + +class EvaluationStrategy(ABC): + """ + Abstract class for evaluation strategies + """ + + @abstractmethod + def evaluate(self, input_text: str, output_text: str, **kwargs): + pass + + +class LLMEvaluationStrategy(EvaluationStrategy): + """ + Strategy using an LLM for evaluation + """ + + def __init__(self, model: str, config: dict): + self.model = model + self.config = config + + def _get_prompt(self, input_text: str, output_text: str, instructions: str): + prompt = "" + if instructions: + prompt = f"Evaluate the following output given the input and instructions:\n" + prompt += f"Instructions: {instructions}\n" + prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" + else: + prompt = f"Note: The score should be between 0 and 100. The higher the score, the better the output." + prompt += f"Evaluate the following output given the input:\n" + prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" + return prompt + + def _validate_response_conformity(self, response: str, instructions: str): + if instructions: + prompt = f"These are the instructions: {instructions}\n" + prompt += f"This is the Evaluator's response: {response}\n" + prompt += f"Check if the response is consistent with the instructions. If it is, return 'True'. If it is not, return 'False'." + llm_response_service = LLMResponseService(model=self.model, prompt=[{"role": "user", "content": prompt}]) + response = llm_response_service.get_response() + if response == "True": + logger.info(f"Response is consistent with the instructions. Response: {response}") + return True + else: + logger.error(f"Response is not consistent with the instructions. Response: {response}") + return False + else: + # If there are no instructions, check if score is between 0 and 100 + if 0 <= int(response) <= 100: + logger.info(f"Response is between 0 and 100. Response: {response}") + return True + else: + logger.error(f"Response is not between 0 and 100. Response: {response}") + return False + + def evaluate(self, input_text: str, output_text: str, instructions: str, max_retries: int = 3): + prompt = self._get_prompt(input_text, output_text, instructions) + llm_response_service = LLMResponseService(model=self.model, prompt=[{"role": "user", "content": prompt}]) + response = llm_response_service.get_response() + validation_passed = self._validate_response_conformity(response, instructions) + # If validation failed, re-try again + if not validation_passed: + # re-try again + if max_retries > 0: + return self.evaluate(input_text, output_text, instructions, max_retries - 1) + else: + logger.error(f"Response is not consistent with the instructions. Response: {response}") + return False + + if not validation_passed: + raise ValueError(f"Response is not consistent with the instructions. Response: {response}") + return response + + +class EmbeddingEvaluationStrategy(EvaluationStrategy): + """ + Strategy using embedding similarity for evaluation + """ + + def __init__(self, embedding_model): + self.embedding_model = embedding_model + + def evaluate(self, input_text: str, output_text: str, **kwargs): + # Compute embeddings for input and output + input_emb = self.embedding_model.encode(input_text) + output_emb = self.embedding_model.encode(output_text) + cosine_similarity = np.dot(input_emb, output_emb) / ( + np.linalg.norm(input_emb) * np.linalg.norm(output_emb) + 1e-8 + ) + return cosine_similarity + + +class FuzzyEvaluationStrategy(EvaluationStrategy): + """ + Strategy using fuzzy matching for evaluation + """ + + def evaluate(self, input_text: str, output_text: str, **kwargs): + score = fuzz.ratio(input_text, output_text) + return score + + +class Evaluator: + """ + Evaluator class takes instructions, config and a strategy instance + """ + + def __init__(self, instructions: str, config: dict, strategy: EvaluationStrategy): + self.instructions = instructions + self.config = config + self.strategy = strategy + + def evaluate_pair(self, input_text: str, output_text: str): + score = self.strategy.evaluate(input_text, output_text, instructions=self.instructions) + return { + "instructions": self.instructions, + "input": input_text, + "output": output_text, + "score": score, + "config": self.config, + } From d37a9d8828e6652e7c021c8d0db21c319c4685f5 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 03:39:02 +0530 Subject: [PATCH 30/62] added evaluation routes --- server/app/__init__.py | 2 + server/app/api/evaluation_routes.py | 61 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index c1ea571..ea04f81 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -9,6 +9,7 @@ from .api.auth_routes import auth_bp from .api.config_routes import config_bp from .api.dataset_routes import dataset_bp +from .api.evaluation_routes import evaluation_bp from .api.main import main_bp from .api.project_routes import projects_bp from .config import Config, Logging @@ -23,6 +24,7 @@ def register_blueprints(app): app.register_blueprint(ai_agent_bp, url_prefix="/api/ai_agents") app.register_blueprint(dataset_bp, url_prefix="/api/datasets") app.register_blueprint(config_bp, url_prefix="/api/config") + app.register_blueprint(evaluation_bp, url_prefix="/api/evaluation") app.register_blueprint(auth_bp, url_prefix="/api/auth") diff --git a/server/app/api/evaluation_routes.py b/server/app/api/evaluation_routes.py index 9c5bb6d..a8d492c 100644 --- a/server/app/api/evaluation_routes.py +++ b/server/app/api/evaluation_routes.py @@ -1 +1,60 @@ -# to be filled +from bson import ObjectId +from flask import Blueprint, jsonify, request + +from .. import ( + EmbeddingEvaluationStrategy, + Evaluator, + FuzzyEvaluationStrategy, + LLMEvaluationStrategy, +) +from ..config import Logging +from ..models import Project + +logger = Logging.get_logger(__name__) +evaluation_bp = Blueprint("evaluation", __name__) + + +@evaluation_bp.route("/", methods=["POST"]) +def evaluate(): + """ + Evaluate the output of an agent against the input + """ + try: + data = request.json + project_id = data.get("project_id") + agent_id = data.get("agent_id") + input_text = data.get("input_text") + output_text = data.get("output_text") + + if not all([project_id, agent_id, input_text, output_text]): + return jsonify({"error": "project_id, agent_id, input_text and output_text are required"}), 400 + + project = Project.objects.get(id=ObjectId(project_id)) + try: + agent = project.ai_agents[int(agent_id)] + except (IndexError, ValueError): + return jsonify({"error": "Invalid agent reference"}), 400 + + evaluator_config = agent.agent_config or {} + strategy_type = evaluator_config.get("strategy", "llm").lower() + if strategy_type == "llm": + evaluator_strategy = LLMEvaluationStrategy( + model=evaluator_config.get("model", "default-model"), config=evaluator_config + ) + elif strategy_type == "embedding": + embedding_model = evaluator_config.get("embedding_model") + if not embedding_model: + return jsonify({"error": "Embedding model not provided in evaluator config"}), 400 + evaluator_strategy = EmbeddingEvaluationStrategy(embedding_model=embedding_model) + elif strategy_type == "fuzzy": + evaluator_strategy = FuzzyEvaluationStrategy() + else: + return jsonify({"error": "Unknown evaluation strategy"}), 400 + + instructions = agent.prompt[0]["content"] if agent.prompt else "" + evaluator = Evaluator(instructions=instructions, config=evaluator_config, strategy=evaluator_strategy) + score = evaluator.evaluate_pair(input_text, output_text) + return jsonify({"score": score}), 200 + except Exception as e: + logger.error(f"Error during evaluation: {e}") + return jsonify({"error": str(e)}), 500 From 113f49ed3e5694dba980456639990be869555c2b Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 03:46:54 +0530 Subject: [PATCH 31/62] using id instead of index in variant --- server/app/api/ai_agent_routes.py | 76 +++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py index 5ed56fb..5970ea5 100644 --- a/server/app/api/ai_agent_routes.py +++ b/server/app/api/ai_agent_routes.py @@ -59,13 +59,15 @@ def create_agent(project_id): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//agents/", methods=["PUT"]) +@ai_agent_bp.route("//agents/", methods=["PUT"]) @login_required -def update_agent(project_id, agent_index): +def update_agent(project_id, agent_id): try: data = request.json project = Project.objects.get(id=ObjectId(project_id)) - agent = project.ai_agents[int(agent_index)] + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 agent.name = data.get("name", agent.name) agent.agent_config = data.get("config", agent.agent_config) agent.prompt = data.get("prompt", agent.prompt) @@ -78,12 +80,15 @@ def update_agent(project_id, agent_index): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//agents/", methods=["DELETE"]) +@ai_agent_bp.route("//agents/", methods=["DELETE"]) @login_required -def remove_agent(project_id, agent_index): +def remove_agent(project_id, agent_id): try: project = Project.objects.get(id=ObjectId(project_id)) - del project.ai_agents[int(agent_index)] + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + project.ai_agents.remove(agent) project.save() logger.info("AI Agent deleted successfully!") return jsonify({"message": "AI Agent deleted successfully!"}), 200 @@ -92,16 +97,34 @@ def remove_agent(project_id, agent_index): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//agents//variants", methods=["POST"]) +@ai_agent_bp.route("//agents//variants", methods=["GET"]) @login_required -def create_variant(project_id, agent_index): +def get_variants(project_id, agent_id): + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) + logger.info("Successfully fetched variants") + return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 + except Exception as e: + logger.error(f"Error in fetching variants: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +@ai_agent_bp.route("//agents//variants", methods=["POST"]) +@login_required +def create_variant(project_id, agent_id): try: data = request.json variant_name = data.get("name") variables = data.get("variables") project = Project.objects.get(id=ObjectId(project_id)) - agent = project.ai_agents[int(agent_index)] + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 variable_names = variables.keys() if not set(variable_names).issubset(set(agent.included_variables)): @@ -118,27 +141,40 @@ def create_variant(project_id, agent_index): return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//agents//variants", methods=["GET"]) +@ai_agent_bp.route("//agents//variants/", methods=["PUT"]) @login_required -def get_variants(project_id, agent_index): +def update_variant(project_id, agent_id, variant_id): try: + data = request.json project = Project.objects.get(id=ObjectId(project_id)) - agent = project.ai_agents[int(agent_index)] - serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) - logger.info("Successfully fetched variants") - return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + variant.name = data.get("name", variant.name) + variant.variables = data.get("variables", variant.variables) + project.save() + logger.info("Variant updated successfully") + return jsonify({"message": "Variant updated successfully"}), 200 except Exception as e: - logger.error(f"Error in fetching variants: {str(e)}") + logger.error(f"Error in updating variant: {str(e)}") return jsonify({"error": str(e)}), 500 -@ai_agent_bp.route("//agents//variants/", methods=["DELETE"]) +@ai_agent_bp.route("//agents//variants/", methods=["DELETE"]) @login_required -def remove_variant(project_id, agent_index, variant_index): +def remove_variant(project_id, agent_id, variant_id): try: project = Project.objects.get(id=ObjectId(project_id)) - agent = project.ai_agents[int(agent_index)] - del agent.variants[int(variant_index)] + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + agent.variants.remove(variant) project.save() logger.info("Variant deleted successfully!") return jsonify({"message": "Variant deleted successfully!"}), 200 From 257c4bd539d369babd64b63d3137a11c075e9a43 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 04:11:37 +0530 Subject: [PATCH 32/62] moved from blueprints to viewsets --- requirements.txt | Bin 492 -> 540 bytes server/app/__init__.py | 50 ++++--- server/app/api/ai_agent_routes.py | 206 -------------------------- server/app/api/ai_agent_viewset.py | 210 +++++++++++++++++++++++++++ server/app/api/auth_routes.py | 63 -------- server/app/api/auth_viewset.py | 52 +++++++ server/app/api/config_routes.py | 10 -- server/app/api/config_viewset.py | 12 ++ server/app/api/dataset_routes.py | 145 ------------------ server/app/api/dataset_viewset.py | 136 +++++++++++++++++ server/app/api/evaluation_routes.py | 60 -------- server/app/api/evaluation_viewset.py | 58 ++++++++ server/app/api/main.py | 17 --- server/app/api/main_viewset.py | 18 +++ server/app/api/project_routes.py | 104 ------------- server/app/api/project_viewset.py | 93 ++++++++++++ 16 files changed, 605 insertions(+), 629 deletions(-) delete mode 100644 server/app/api/ai_agent_routes.py create mode 100644 server/app/api/ai_agent_viewset.py delete mode 100644 server/app/api/auth_routes.py create mode 100644 server/app/api/auth_viewset.py delete mode 100644 server/app/api/config_routes.py create mode 100644 server/app/api/config_viewset.py delete mode 100644 server/app/api/dataset_routes.py create mode 100644 server/app/api/dataset_viewset.py delete mode 100644 server/app/api/evaluation_routes.py create mode 100644 server/app/api/evaluation_viewset.py delete mode 100644 server/app/api/main.py create mode 100644 server/app/api/main_viewset.py delete mode 100644 server/app/api/project_routes.py create mode 100644 server/app/api/project_viewset.py diff --git a/requirements.txt b/requirements.txt index 40f52e6d16731051f4471adbbeeb677e22747890..c0476d85b1729c499823a785b310e24f6d0d262a 100644 GIT binary patch delta 50 xcmaFEJcnh%0d+Tq9EL=OVuox6T?S_;yO<%3p%lor1wsP`JqAMtvyIzL838_y3qJq= delta 10 RcmbQk@`icBfsG$@7y%l{1g-!8 diff --git a/server/app/__init__.py b/server/app/__init__.py index ea04f81..48eb2de 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -2,16 +2,16 @@ import certifi import mongoengine -from flask import Flask +from flask import Blueprint, Flask from flask_cors import CORS -from .api.ai_agent_routes import ai_agent_bp -from .api.auth_routes import auth_bp -from .api.config_routes import config_bp -from .api.dataset_routes import dataset_bp -from .api.evaluation_routes import evaluation_bp -from .api.main import main_bp -from .api.project_routes import projects_bp +from .api.ai_agent_viewset import AIAgentVariantViewSet, AIAgentViewSet +from .api.auth_viewset import AuthViewSet +from .api.config_viewset import ConfigViewSet +from .api.dataset_viewset import DatasetViewSet +from .api.evaluation_viewset import EvaluationViewSet +from .api.main_viewset import MainViewSet +from .api.project_viewset import ProjectViewSet from .config import Config, Logging # Initialize logging once @@ -19,32 +19,34 @@ def register_blueprints(app): - app.register_blueprint(main_bp) - app.register_blueprint(projects_bp, url_prefix="/api/projects") - app.register_blueprint(ai_agent_bp, url_prefix="/api/ai_agents") - app.register_blueprint(dataset_bp, url_prefix="/api/datasets") - app.register_blueprint(config_bp, url_prefix="/api/config") - app.register_blueprint(evaluation_bp, url_prefix="/api/evaluation") - app.register_blueprint(auth_bp, url_prefix="/api/auth") + # Create an API blueprint + api_bp = Blueprint("api", __name__, url_prefix="/api") + + # Register viewsets with their route bases. + AIAgentViewSet.register(api_bp) + AIAgentVariantViewSet.register(api_bp) + AuthViewSet.register(api_bp) + ConfigViewSet.register(api_bp) + DatasetViewSet.register(api_bp) + EvaluationViewSet.register(api_bp) + MainViewSet.register(api_bp) + ProjectViewSet.register(api_bp) + + # Register the API blueprint + app.register_blueprint(api_bp) def create_app(config_class=Config): + """ + Create a Flask app with the given configuration. + """ app = Flask(__name__, static_folder="../../src/static", template_folder="../../src/templates") - - # Connect to MongoDB mongoengine.connect( host=config_class.MONGO_URI, ssl=True, tlscafile=certifi.where(), ) - - # Load config app.config.from_object(config_class) - - # Initialize CORS CORS(app) - - # Register blueprints register_blueprints(app) - return app diff --git a/server/app/api/ai_agent_routes.py b/server/app/api/ai_agent_routes.py deleted file mode 100644 index 5970ea5..0000000 --- a/server/app/api/ai_agent_routes.py +++ /dev/null @@ -1,206 +0,0 @@ -from bson import ObjectId -from flask import Blueprint, Response, jsonify, request, stream_with_context - -from ..auth.decorators import login_required -from ..config import Logging -from ..core.services.ai_agent_services import generate_conversation_service -from ..models import AIAgent, AIAgentVariant, Project -from ..serializers import AIAgentSerializer, AIAgentVariantSerializer - -logger = Logging.get_logger(__name__) - -ai_agent_bp = Blueprint("ai_agents", __name__) - - -@ai_agent_bp.route("//agents", methods=["GET"]) -@login_required -def get_agents(project_id): - try: - project_id = ObjectId(project_id) - project = Project.objects.get(id=project_id) - serialized_agents = AIAgentSerializer(many=True).dump(project.ai_agents) - logger.info(f"Successfully fetched ai agents for the project: {str(project_id)}") - return ( - jsonify( - { - "message": f"Successfully fetched ai agents for the project: {str(project_id)}", - "data": serialized_agents, - } - ), - 200, - ) - except Exception as e: - logger.error(f"Error in fetching AI agents: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents", methods=["POST"]) -@login_required -def create_agent(project_id): - try: - data = request.json - project = Project.objects.get(id=ObjectId(project_id)) - - # Create an AIAgent instance - new_agent = AIAgent( - name=data["name"], - agent_config=data["config"], - prompt=data["prompt"], - included_variables=data["included_variables"], - is_alive=True, - ) - - project.ai_agents.append(new_agent) - project.save() - logger.info("AI Agent created successfully") - return jsonify({"message": "AI Agent created successfully"}), 201 - except Exception as e: - logger.error(f"Error in creating AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents/", methods=["PUT"]) -@login_required -def update_agent(project_id, agent_id): - try: - data = request.json - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - agent.name = data.get("name", agent.name) - agent.agent_config = data.get("config", agent.agent_config) - agent.prompt = data.get("prompt", agent.prompt) - agent.included_variables = data.get("included_variables", agent.included_variables) - project.save() - logger.info("AI Agent updated successfully") - return jsonify({"message": "AI Agent updated successfully"}), 200 - except Exception as e: - logger.error(f"Error in updating AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents/", methods=["DELETE"]) -@login_required -def remove_agent(project_id, agent_id): - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - project.ai_agents.remove(agent) - project.save() - logger.info("AI Agent deleted successfully!") - return jsonify({"message": "AI Agent deleted successfully!"}), 200 - except Exception as e: - logger.error(f"Error in deleting AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents//variants", methods=["GET"]) -@login_required -def get_variants(project_id, agent_id): - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) - logger.info("Successfully fetched variants") - return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 - except Exception as e: - logger.error(f"Error in fetching variants: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents//variants", methods=["POST"]) -@login_required -def create_variant(project_id, agent_id): - try: - data = request.json - variant_name = data.get("name") - variables = data.get("variables") - - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - - variable_names = variables.keys() - if not set(variable_names).issubset(set(agent.included_variables)): - return jsonify({"error": "Variables not included in the agent"}), 400 - - # Create an AIAgentVariant instance - new_variant = AIAgentVariant(name=variant_name, variables=variables, is_alive=True) - agent.variants.append(new_variant) - project.save() - logger.info("Variant created successfully") - return jsonify({"message": "Variant created successfully"}), 201 - except Exception as e: - logger.error(f"Error in creating variant: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents//variants/", methods=["PUT"]) -@login_required -def update_variant(project_id, agent_id, variant_id): - try: - data = request.json - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) - if not variant: - return jsonify({"error": "Variant not found"}), 404 - variant.name = data.get("name", variant.name) - variant.variables = data.get("variables", variant.variables) - project.save() - logger.info("Variant updated successfully") - return jsonify({"message": "Variant updated successfully"}), 200 - except Exception as e: - logger.error(f"Error in updating variant: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("//agents//variants/", methods=["DELETE"]) -@login_required -def remove_variant(project_id, agent_id, variant_id): - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) - if not variant: - return jsonify({"error": "Variant not found"}), 404 - agent.variants.remove(variant) - project.save() - logger.info("Variant deleted successfully!") - return jsonify({"message": "Variant deleted successfully!"}), 200 - except Exception as e: - logger.error(f"Error in deleting variant: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@ai_agent_bp.route("/conversation", methods=["POST"]) -@login_required -def generate_conversation(): - try: - data = request.json - config = data.get("config") - prompt = data.get("prompt") - stream = data.get("stream", True) - - if stream: - logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") - return Response( - stream_with_context(generate_conversation_service(config, prompt, stream)), - content_type="text/event-stream", - ) - else: - logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") - return jsonify(generate_conversation_service(config, prompt, stream)), 200 - except Exception as e: - logger.error(f"Error in generating conversation: {str(e)}") - return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py new file mode 100644 index 0000000..7856796 --- /dev/null +++ b/server/app/api/ai_agent_viewset.py @@ -0,0 +1,210 @@ +from bson import ObjectId +from flask import Response, jsonify, request, stream_with_context +from flask_classful import FlaskView, route + +from ..auth.decorators import login_required +from ..config import Logging +from ..core.services.ai_agent_services import generate_conversation_service +from ..models import AIAgent, AIAgentVariant, Project +from ..serializers import AIAgentSerializer, AIAgentVariantSerializer + +logger = Logging.get_logger(__name__) + + +class AIAgentViewSet(FlaskView): + """ + ViewSet for AI Agents. + URL Prefix: //agents + """ + + decorators = [login_required] + route_prefix = "//agents" + + def index(self, project_id): + """GET //agents -> List agents.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + serialized_agents = AIAgentSerializer(many=True).dump(project.ai_agents) + logger.info(f"Successfully fetched AI agents for project: {project_id}") + return ( + jsonify( + { + "message": f"Successfully fetched AI agents for project: {project_id}", + "data": serialized_agents, + } + ), + 200, + ) + except Exception as e: + logger.error(f"Error fetching AI agents: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def post(self, project_id): + """POST //agents -> Create a new agent.""" + try: + data = request.json + project = Project.objects.get(id=ObjectId(project_id)) + new_agent = AIAgent( + name=data["name"], + agent_config=data["config"], + prompt=data["prompt"], + included_variables=data["included_variables"], + is_alive=True, + ) + project.ai_agents.append(new_agent) + project.save() + logger.info("AI Agent created successfully") + return jsonify({"message": "AI Agent created successfully"}), 201 + except Exception as e: + logger.error(f"Error creating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def get(self, project_id, agent_id): + """GET //agents/ -> Retrieve a single agent.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if agent: + return jsonify(AIAgentSerializer().dump(agent)), 200 + return jsonify({"error": "Agent not found"}), 404 + except Exception as e: + logger.error(f"Error retrieving AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self, project_id, agent_id): + """PUT //agents/ -> Update an agent.""" + try: + data = request.json + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + agent.name = data.get("name", agent.name) + agent.agent_config = data.get("config", agent.agent_config) + agent.prompt = data.get("prompt", agent.prompt) + agent.included_variables = data.get("included_variables", agent.included_variables) + project.save() + logger.info("AI Agent updated successfully") + return jsonify({"message": "AI Agent updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def delete(self, project_id, agent_id): + """DELETE //agents/ -> Remove an agent.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + project.ai_agents.remove(agent) + project.save() + logger.info("AI Agent deleted successfully") + return jsonify({"message": "AI Agent deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + @route("/conversation", methods=["POST"], endpoint="conversation") + def generate_conversation(self): + """POST /agents/conversation -> Generate a conversation.""" + try: + data = request.json + config = data.get("config") + prompt = data.get("prompt") + stream = data.get("stream", True) + if stream: + logger.info(f"Streaming conversation for prompt: {prompt}") + return Response( + stream_with_context(generate_conversation_service(config, prompt, stream)), + content_type="text/event-stream", + ) + else: + logger.info(f"Generating conversation for prompt: {prompt} without stream") + return jsonify(generate_conversation_service(config, prompt, stream)), 200 + except Exception as e: + logger.error(f"Error generating conversation: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +class AIAgentVariantViewSet(FlaskView): + """ + ViewSet for AI Agent Variants. + URL Prefix: //agents//variants + """ + + decorators = [login_required] + route_prefix = "//agents//variants" + + def index(self, project_id, agent_id): + """GET -> List variants for an agent.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) + logger.info("Successfully fetched variants") + return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 + except Exception as e: + logger.error(f"Error fetching variants: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def post(self, project_id, agent_id): + """POST -> Create a new variant for an agent.""" + try: + data = request.json + variant_name = data.get("name") + variables = data.get("variables") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + if not set(variables.keys()).issubset(set(agent.included_variables)): + return jsonify({"error": "Variables not included in the agent"}), 400 + new_variant = AIAgentVariant(name=variant_name, variables=variables, is_alive=True) + agent.variants.append(new_variant) + project.save() + logger.info("Variant created successfully") + return jsonify({"message": "Variant created successfully"}), 201 + except Exception as e: + logger.error(f"Error creating variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self, project_id, agent_id, variant_id): + """PUT -> Update an agent variant.""" + try: + data = request.json + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + variant.name = data.get("name", variant.name) + variant.variables = data.get("variables", variant.variables) + project.save() + logger.info("Variant updated successfully") + return jsonify({"message": "Variant updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def delete(self, project_id, agent_id, variant_id): + """DELETE -> Remove an agent variant.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + agent.variants.remove(variant) + project.save() + logger.info("Variant deleted successfully") + return jsonify({"message": "Variant deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting variant: {str(e)}") + return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/auth_routes.py b/server/app/api/auth_routes.py deleted file mode 100644 index b69662d..0000000 --- a/server/app/api/auth_routes.py +++ /dev/null @@ -1,63 +0,0 @@ -from flask import Blueprint, jsonify, request, session - -from ..auth.handlers import AuthHandler -from ..config import Logging -from ..models import User -from ..utils.datetime_utils import get_datetime_in_ist - -auth_bp = Blueprint("auth", __name__) -logger = Logging.get_logger(__name__) - - -@auth_bp.route("/verify", methods=["POST"]) -def verify_token(): - """Verify Firebase token and create session""" - data = request.get_json() - id_token = data.get("idToken") - - if not id_token: - return jsonify({"error": "No token provided"}), 400 - - decoded_token = AuthHandler.verify_firebase_token(id_token) - if not decoded_token: - return jsonify({"error": "Invalid token"}), 401 - - # check if user exists in database - user = User.objects.filter(firebase_uid=decoded_token.get("uid")).first() - if not user: - # User does not exist, create a new one - logger.info(f"Creating new user: {decoded_token.get('email')}") - user = User( - name=decoded_token.get("name"), - email=decoded_token.get("email"), - picture=decoded_token.get("picture"), - firebase_uid=decoded_token.get("uid"), - is_alive=True, - ) - user.save() - - # if user is not active - if not user.is_alive: - logger.info(f"User account has been deactivated: {decoded_token.get('email')}") - return jsonify({"error": "User account has been deactivated"}), 401 - - logger.info(f"User logged in: {user.email}") - AuthHandler.create_session(decoded_token) - return jsonify({"success": True, "user": session["user"]}) - - -@auth_bp.route("/logout", methods=["POST"]) -def logout(): - """Clear user session""" - logger.info(f"User logged out: {session.get('user').get('email')}") - AuthHandler.clear_session() - return jsonify({"success": True}) - - -@auth_bp.route("/session") -def get_session(): - """Get current session info""" - user = session.get("user") - if user: - return jsonify({"user": user}) - return jsonify({"user": None}), 401 diff --git a/server/app/api/auth_viewset.py b/server/app/api/auth_viewset.py new file mode 100644 index 0000000..140a6c6 --- /dev/null +++ b/server/app/api/auth_viewset.py @@ -0,0 +1,52 @@ +from flask import jsonify, request, session +from flask_classful import FlaskView + +from ..auth.handlers import AuthHandler +from ..config import Logging +from ..models import User + +logger = Logging.get_logger(__name__) + + +class AuthViewSet(FlaskView): + route_prefix = "/auth" + + def post_verify(self): + """POST /auth/verify -> Verify Firebase token and create session.""" + data = request.get_json() + id_token = data.get("idToken") + if not id_token: + return jsonify({"error": "No token provided"}), 400 + decoded_token = AuthHandler.verify_firebase_token(id_token) + if not decoded_token: + return jsonify({"error": "Invalid token"}), 401 + user = User.objects.filter(firebase_uid=decoded_token.get("uid")).first() + if not user: + logger.info(f"Creating new user: {decoded_token.get('email')}") + user = User( + name=decoded_token.get("name"), + email=decoded_token.get("email"), + picture=decoded_token.get("picture"), + firebase_uid=decoded_token.get("uid"), + is_alive=True, + ) + user.save() + if not user.is_alive: + logger.info(f"User account deactivated: {decoded_token.get('email')}") + return jsonify({"error": "User account has been deactivated"}), 401 + logger.info(f"User logged in: {user.email}") + AuthHandler.create_session(decoded_token) + return jsonify({"success": True, "user": session["user"]}) + + def post_logout(self): + """POST /auth/logout -> Clear user session.""" + logger.info(f"User logged out: {session.get('user', {}).get('email')}") + AuthHandler.clear_session() + return jsonify({"success": True}) + + def get_session(self): + """GET /auth/session -> Retrieve current session info.""" + user = session.get("user") + if user: + return jsonify({"user": user}) + return jsonify({"user": None}), 401 diff --git a/server/app/api/config_routes.py b/server/app/api/config_routes.py deleted file mode 100644 index e6e391c..0000000 --- a/server/app/api/config_routes.py +++ /dev/null @@ -1,10 +0,0 @@ -from flask import Blueprint, jsonify - -from ..utils.model_map import LLM_MODELS - -config_bp = Blueprint("config", __name__) - - -@config_bp.route("/models", methods=["GET"]) -def get_models(): - return jsonify(LLM_MODELS) diff --git a/server/app/api/config_viewset.py b/server/app/api/config_viewset.py new file mode 100644 index 0000000..ab9a10b --- /dev/null +++ b/server/app/api/config_viewset.py @@ -0,0 +1,12 @@ +from flask import jsonify +from flask_classful import FlaskView, route + +from ..utils.model_map import LLM_MODELS + + +class ConfigViewSet(FlaskView): + route_base = "/config" + + @route("/models", methods=["GET"]) + def get_models(self): + return jsonify(LLM_MODELS) diff --git a/server/app/api/dataset_routes.py b/server/app/api/dataset_routes.py deleted file mode 100644 index 181f34f..0000000 --- a/server/app/api/dataset_routes.py +++ /dev/null @@ -1,145 +0,0 @@ -from bson import ObjectId -from flask import Blueprint, jsonify, request - -from ..auth.decorators import login_required -from ..config import Logging -from ..models import Dataset, Project -from ..serializers import DatasetSerializer -from ..utils.datetime_utils import get_datetime_in_ist - -logger = Logging.get_logger(__name__) - -dataset_bp = Blueprint("datasets", __name__) - - -@dataset_bp.route("/", methods=["GET"]) -@login_required -def get_datasets(project_id): - try: - project_id = ObjectId(project_id) - project = Project.objects.get(id=project_id) - datasets = Dataset.objects.filter(project=project, is_alive=True) - serialized_datasets = DatasetSerializer(many=True).dump(datasets) - logger.info(f"Successfully fetched datasets for the project: {str(project_id)}") - return jsonify({"message": "Successfully fetched datasets", "data": serialized_datasets}) - except Exception as e: - logger.error(f"Error in fetching datasets: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("/", methods=["POST"]) -@login_required -def create_dataset(): - try: - data = request.json - project_id = ObjectId(data["project_id"]) - project = Project.objects.get(id=project_id) - dataset = Dataset( - project=project, - name=data["name"], - type=data["type"], - samples=data.get("samples", []), - is_alive=True, - ).save() - logger.info(f"Dataset '{data['name']}' created successfully") - return jsonify({"message": f"Dataset '{data['name']}' created successfully", "id": str(dataset.id)}), 201 - except Exception as e: - logger.error(f"Error in creating dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("/", methods=["PUT"]) -@login_required -def update_dataset(dataset_id): - try: - data = request.json - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - dataset.name = data.get("name", dataset.name) - dataset.samples = data.get("samples", dataset.samples) - dataset.save() - logger.info(f"Dataset '{data.get('name', dataset.name)}' updated successfully") - return jsonify({"message": f"Dataset '{data.get('name', dataset.name)}' updated successfully"}), 200 - except Exception as e: - logger.error(f"Error in updating dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("/", methods=["DELETE"]) -@login_required -def remove_dataset(dataset_id): - try: - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - dataset.delete() - logger.info(f"Dataset deleted successfully! with id: {dataset_id}") - return jsonify({"message": "Dataset deleted successfully! with id: " + dataset_id}), 200 - except Exception as e: - logger.error(f"Error in deleting dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("//samples", methods=["POST"]) -@login_required -def add_samples(dataset_id): - try: - data = request.json - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - sample = data.get("sample") or {} - if sample in dataset.samples: - return jsonify({"message": "Sample already exists in the dataset"}), 400 - dataset.samples.append(sample) - dataset.save() - logger.info(f"Sample added to dataset '{dataset_id}' successfully") - return jsonify({"message": f"Sample added to dataset '{dataset_id}' successfully"}), 200 - except Exception as e: - logger.error(f"Error in adding sample to dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("//samples", methods=["DELETE"]) -@login_required -def remove_samples(dataset_id): - try: - data = request.json - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - sample = data.get("sample") or {} - if sample in dataset.samples: - dataset.samples.remove(sample) - dataset.save() - logger.info(f"Sample removed from dataset '{dataset_id}' successfully") - return jsonify({"message": f"Sample removed from dataset '{dataset_id}' successfully"}), 200 - else: - return jsonify({"message": "Sample not found in dataset"}), 404 - except Exception as e: - logger.error(f"Error in removing sample from dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 - - -@dataset_bp.route("//samples", methods=["PUT"]) -@login_required -def update_sample(dataset_id): - try: - data = request.json - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - sample = data.get("sample") or {} - updated_sample = data.get("updated_sample") or {} - try: - index = dataset.samples.index(sample) - except ValueError: - return jsonify({"message": "Sample not found in dataset"}), 404 - dataset.samples[index] = updated_sample - dataset.save() - logger.info(f"Sample updated in dataset '{dataset_id}' successfully") - return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}), 200 - except Exception as e: - logger.error(f"Error in updating sample in dataset: {str(e)}") - return jsonify({"message": str(e)}), 500 diff --git a/server/app/api/dataset_viewset.py b/server/app/api/dataset_viewset.py new file mode 100644 index 0000000..853898b --- /dev/null +++ b/server/app/api/dataset_viewset.py @@ -0,0 +1,136 @@ +from bson import ObjectId +from flask import jsonify, request +from flask_classful import FlaskView, route + +from ..auth.decorators import login_required +from ..config import Logging +from ..models import Dataset, Project +from ..serializers import DatasetSerializer + +logger = Logging.get_logger(__name__) + + +class DatasetViewSet(FlaskView): + decorators = [login_required] + route_prefix = "/datasets" + + @route("/project/", methods=["GET"]) + def index_by_project(self, project_id): + """GET /datasets/project/ -> Get datasets for a project.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + datasets = Dataset.objects.filter(project=project, is_alive=True) + serialized = DatasetSerializer(many=True).dump(datasets) + logger.info(f"Fetched datasets for project: {project_id}") + return jsonify({"message": "Successfully fetched datasets", "data": serialized}) + except Exception as e: + logger.error(f"Error fetching datasets: {str(e)}") + return jsonify({"message": str(e)}), 500 + + def post(self): + """POST /datasets -> Create a new dataset.""" + try: + data = request.json + project = Project.objects.get(id=ObjectId(data["project_id"])) + dataset = Dataset( + project=project, + name=data["name"], + type=data["type"], + samples=data.get("samples", []), + is_alive=True, + ).save() + logger.info(f"Dataset '{data['name']}' created successfully") + return jsonify({"message": f"Dataset '{data['name']}' created successfully", "id": str(dataset.id)}), 201 + except Exception as e: + logger.error(f"Error creating dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + def put(self, dataset_id): + """PUT /datasets/ -> Update a dataset.""" + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + dataset.name = data.get("name", dataset.name) + dataset.samples = data.get("samples", dataset.samples) + dataset.save() + logger.info(f"Dataset '{dataset.name}' updated successfully") + return jsonify({"message": f"Dataset '{dataset.name}' updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + def delete(self, dataset_id): + """DELETE /datasets/ -> Delete a dataset.""" + try: + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + dataset.delete() + logger.info(f"Dataset deleted successfully with id: {dataset_id}") + return jsonify({"message": "Dataset deleted successfully with id: " + dataset_id}), 200 + except Exception as e: + logger.error(f"Error deleting dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + @route("//samples", methods=["POST"]) + def add_samples(self, dataset_id): + """POST /datasets//samples -> Add a sample.""" + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + if sample in dataset.samples: + return jsonify({"message": "Sample already exists"}), 400 + dataset.samples.append(sample) + dataset.save() + logger.info(f"Sample added to dataset '{dataset_id}'") + return jsonify({"message": f"Sample added to dataset '{dataset_id}' successfully"}), 200 + except Exception as e: + logger.error(f"Error adding sample: {str(e)}") + return jsonify({"message": str(e)}), 500 + + @route("//samples", methods=["DELETE"]) + def remove_samples(self, dataset_id): + """DELETE /datasets//samples -> Remove a sample.""" + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + if sample in dataset.samples: + dataset.samples.remove(sample) + dataset.save() + logger.info(f"Sample removed from dataset '{dataset_id}'") + return jsonify({"message": f"Sample removed from dataset '{dataset_id}' successfully"}), 200 + else: + return jsonify({"message": "Sample not found"}), 404 + except Exception as e: + logger.error(f"Error removing sample: {str(e)}") + return jsonify({"message": str(e)}), 500 + + @route("//samples", methods=["PUT"]) + def update_sample(self, dataset_id): + """PUT /datasets//samples -> Update a sample.""" + try: + data = request.json + dataset = Dataset.objects.get(id=dataset_id) + if not dataset: + return jsonify({"message": "Dataset not found"}), 404 + sample = data.get("sample") or {} + updated_sample = data.get("updated_sample") or {} + try: + index = dataset.samples.index(sample) + except ValueError: + return jsonify({"message": "Sample not found"}), 404 + dataset.samples[index] = updated_sample + dataset.save() + logger.info(f"Sample updated in dataset '{dataset_id}'") + return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}), 200 + except Exception as e: + logger.error(f"Error updating sample: {str(e)}") + return jsonify({"message": str(e)}), 500 diff --git a/server/app/api/evaluation_routes.py b/server/app/api/evaluation_routes.py deleted file mode 100644 index a8d492c..0000000 --- a/server/app/api/evaluation_routes.py +++ /dev/null @@ -1,60 +0,0 @@ -from bson import ObjectId -from flask import Blueprint, jsonify, request - -from .. import ( - EmbeddingEvaluationStrategy, - Evaluator, - FuzzyEvaluationStrategy, - LLMEvaluationStrategy, -) -from ..config import Logging -from ..models import Project - -logger = Logging.get_logger(__name__) -evaluation_bp = Blueprint("evaluation", __name__) - - -@evaluation_bp.route("/", methods=["POST"]) -def evaluate(): - """ - Evaluate the output of an agent against the input - """ - try: - data = request.json - project_id = data.get("project_id") - agent_id = data.get("agent_id") - input_text = data.get("input_text") - output_text = data.get("output_text") - - if not all([project_id, agent_id, input_text, output_text]): - return jsonify({"error": "project_id, agent_id, input_text and output_text are required"}), 400 - - project = Project.objects.get(id=ObjectId(project_id)) - try: - agent = project.ai_agents[int(agent_id)] - except (IndexError, ValueError): - return jsonify({"error": "Invalid agent reference"}), 400 - - evaluator_config = agent.agent_config or {} - strategy_type = evaluator_config.get("strategy", "llm").lower() - if strategy_type == "llm": - evaluator_strategy = LLMEvaluationStrategy( - model=evaluator_config.get("model", "default-model"), config=evaluator_config - ) - elif strategy_type == "embedding": - embedding_model = evaluator_config.get("embedding_model") - if not embedding_model: - return jsonify({"error": "Embedding model not provided in evaluator config"}), 400 - evaluator_strategy = EmbeddingEvaluationStrategy(embedding_model=embedding_model) - elif strategy_type == "fuzzy": - evaluator_strategy = FuzzyEvaluationStrategy() - else: - return jsonify({"error": "Unknown evaluation strategy"}), 400 - - instructions = agent.prompt[0]["content"] if agent.prompt else "" - evaluator = Evaluator(instructions=instructions, config=evaluator_config, strategy=evaluator_strategy) - score = evaluator.evaluate_pair(input_text, output_text) - return jsonify({"score": score}), 200 - except Exception as e: - logger.error(f"Error during evaluation: {e}") - return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/evaluation_viewset.py b/server/app/api/evaluation_viewset.py new file mode 100644 index 0000000..51ba056 --- /dev/null +++ b/server/app/api/evaluation_viewset.py @@ -0,0 +1,58 @@ +from bson import ObjectId +from flask import jsonify, request +from flask_classful import FlaskView + +from .. import ( + EmbeddingEvaluationStrategy, + Evaluator, + FuzzyEvaluationStrategy, + LLMEvaluationStrategy, +) +from ..config import Logging +from ..models import Project + +logger = Logging.get_logger(__name__) + + +class EvaluationViewSet(FlaskView): + route_prefix = "/evaluation" + + def post_index(self): + """POST /evaluation -> Evaluate an agent's output against input.""" + try: + data = request.json + project_id = data.get("project_id") + agent_id = data.get("agent_id") + input_text = data.get("input_text") + output_text = data.get("output_text") + if not all([project_id, agent_id, input_text, output_text]): + return jsonify({"error": "project_id, agent_id, input_text and output_text are required"}), 400 + project = Project.objects.get(id=ObjectId(project_id)) + try: + agent = project.ai_agents[int(agent_id)] + except (IndexError, ValueError): + return jsonify({"error": "Invalid agent reference"}), 400 + + evaluator_config = agent.agent_config or {} + strategy_type = evaluator_config.get("strategy", "llm").lower() + if strategy_type == "llm": + evaluator_strategy = LLMEvaluationStrategy( + model=evaluator_config.get("model", "default-model"), config=evaluator_config + ) + elif strategy_type == "embedding": + embedding_model = evaluator_config.get("embedding_model") + if not embedding_model: + return jsonify({"error": "Embedding model not provided in evaluator config"}), 400 + evaluator_strategy = EmbeddingEvaluationStrategy(embedding_model=embedding_model) + elif strategy_type == "fuzzy": + evaluator_strategy = FuzzyEvaluationStrategy() + else: + return jsonify({"error": "Unknown evaluation strategy"}), 400 + + instructions = agent.prompt[0]["content"] if agent.prompt else "" + evaluator = Evaluator(instructions=instructions, config=evaluator_config, strategy=evaluator_strategy) + score = evaluator.evaluate_pair(input_text, output_text) + return jsonify({"score": score}), 200 + except Exception as e: + logger.error(f"Error during evaluation: {e}") + return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/main.py b/server/app/api/main.py deleted file mode 100644 index 1ce101d..0000000 --- a/server/app/api/main.py +++ /dev/null @@ -1,17 +0,0 @@ -from flask import Blueprint, redirect, render_template, session - -main_bp = Blueprint("main", __name__) - - -@main_bp.route("/") -def index(): - if "user" not in session: - return redirect("/login") - return render_template("index.html") - - -@main_bp.route("/login") -def login(): - if "user" in session: - return redirect("/") - return render_template("login.html") diff --git a/server/app/api/main_viewset.py b/server/app/api/main_viewset.py new file mode 100644 index 0000000..759ec19 --- /dev/null +++ b/server/app/api/main_viewset.py @@ -0,0 +1,18 @@ +from flask import redirect, render_template, session +from flask_classful import FlaskView + + +class MainViewSet(FlaskView): + route_prefix = "/" + + def index(self): + """GET / -> Homepage; redirects to login if user not in session.""" + if "user" not in session: + return redirect("/login") + return render_template("index.html") + + def login(self): + """GET /login -> Render login page; redirect if already logged in.""" + if "user" in session: + return redirect("/") + return render_template("login.html") diff --git a/server/app/api/project_routes.py b/server/app/api/project_routes.py deleted file mode 100644 index 6f8f019..0000000 --- a/server/app/api/project_routes.py +++ /dev/null @@ -1,104 +0,0 @@ -from bson import ObjectId -from flask import Blueprint, jsonify, request, session - -from ..auth.decorators import login_required -from ..config import Logging -from ..models import Project, User -from ..serializers import ProjectSerializer -from ..utils.datetime_utils import get_datetime_in_ist - -logger = Logging.get_logger(__name__) - -projects_bp = Blueprint("projects", __name__) - - -@projects_bp.route("/", methods=["GET"]) -@login_required -def get_projects(): - try: - firebase_uid = session["user"]["uid"] - user = User.objects.get(firebase_uid=firebase_uid) - projects = Project.objects.filter(user=user, is_alive=True) - serialized_projects = ProjectSerializer(many=True).dump(projects) - logger.info(f"Successfully fetched projects for user: {firebase_uid}") - return jsonify( - { - "message": f"Successfully fetched projects for user_id: {str(user.id)}", - "data": serialized_projects, - } - ) - except Exception as e: - logger.error(f"Error in fetching projects: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@projects_bp.route("/", methods=["GET"]) -@login_required -def get_project(project_id): - try: - project_id = ObjectId(project_id) - project = Project.objects.get(id=project_id) - serialized_project = ProjectSerializer().dump(project) - logger.info(f"Successfully fetched project: {str(project_id)}") - return jsonify( - { - "message": f"Successfully fetched project: {str(project_id)}", - "data": serialized_project, - } - ) - except Exception as e: - logger.error(f"Error in fetching project: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@projects_bp.route("/", methods=["POST"]) -@login_required -def create_project(): - try: - data = request.json - firebase_uid = session["user"]["uid"] - user = User.objects.get(firebase_uid=firebase_uid) - - project = Project( - user=user, - name=data.get("name", "Default Project"), - description=data.get("description", ""), - ai_agents=[], # Initialize empty list for embedded agents - is_alive=True, - ).save() - logger.info(f"Project '{project.name}' created successfully") - return jsonify({"message": f"Project '{project.name}' created successfully", "id": str(project.id)}), 201 - except Exception as e: - logger.error(f"Error in creating project: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@projects_bp.route("/", methods=["PUT"]) -@login_required -def update_project(project_id): - try: - data = request.json - project_id = ObjectId(project_id) - project = Project.objects.get(id=project_id) - project.name = data.get("name", project.name) - project.description = data.get("description", project.description) - project.save() - logger.info(f"Project '{project.name}' updated successfully") - return jsonify({"message": f"Project '{project.name}' updated successfully"}), 200 - except Exception as e: - logger.error(f"Error in updating project: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -@projects_bp.route("/", methods=["DELETE"]) -@login_required -def remove_project(project_id): - try: - project_id = ObjectId(project_id) - project = Project.objects.get(id=project_id) - project.delete() - logger.info("Project deleted successfully!") - return jsonify({"message": "Project deleted successfully!"}), 200 - except Exception as e: - logger.error(f"Error in deleting project: {str(e)}") - return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/project_viewset.py b/server/app/api/project_viewset.py new file mode 100644 index 0000000..293b22a --- /dev/null +++ b/server/app/api/project_viewset.py @@ -0,0 +1,93 @@ +from bson import ObjectId +from flask import jsonify, request, session +from flask_classful import FlaskView + +from ..auth.decorators import login_required +from ..config import Logging +from ..models import Project, User +from ..serializers import ProjectSerializer + +logger = Logging.get_logger(__name__) + + +class ProjectViewSet(FlaskView): + decorators = [login_required] + route_prefix = "/projects" + + def index(self): + """GET /projects -> List projects for the current user.""" + try: + firebase_uid = session["user"]["uid"] + user = User.objects.get(firebase_uid=firebase_uid) + projects = Project.objects.filter(user=user, is_alive=True) + serialized_projects = ProjectSerializer(many=True).dump(projects) + logger.info(f"Fetched projects for user: {firebase_uid}") + return jsonify( + { + "message": f"Successfully fetched projects for user_id: {str(user.id)}", + "data": serialized_projects, + } + ) + except Exception as e: + logger.error(f"Error fetching projects: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def get(self, project_id): + """GET /projects/ -> Fetch a single project.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + serialized_project = ProjectSerializer().dump(project) + logger.info(f"Fetched project: {project_id}") + return jsonify( + { + "message": f"Successfully fetched project: {project_id}", + "data": serialized_project, + } + ) + except Exception as e: + logger.error(f"Error fetching project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def post(self): + """POST /projects -> Create a new project.""" + try: + data = request.json + firebase_uid = session["user"]["uid"] + user = User.objects.get(firebase_uid=firebase_uid) + project = Project( + user=user, + name=data.get("name", "Default Project"), + description=data.get("description", ""), + ai_agents=[], # Initialize empty list for agents + is_alive=True, + ).save() + logger.info(f"Project '{project.name}' created successfully") + return jsonify({"message": f"Project '{project.name}' created successfully", "id": str(project.id)}), 201 + except Exception as e: + logger.error(f"Error creating project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self, project_id): + """PUT /projects/ -> Update a project.""" + try: + data = request.json + project = Project.objects.get(id=ObjectId(project_id)) + project.name = data.get("name", project.name) + project.description = data.get("description", project.description) + project.save() + logger.info(f"Project '{project.name}' updated successfully") + return jsonify({"message": f"Project '{project.name}' updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def delete(self, project_id): + """DELETE /projects/ -> Delete a project.""" + try: + project = Project.objects.get(id=ObjectId(project_id)) + project.delete() + logger.info("Project deleted successfully") + return jsonify({"message": "Project deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting project: {str(e)}") + return jsonify({"error": str(e)}), 500 From 474fff0dadbb72d520c4345140588f3ed9e91272 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 04:15:40 +0530 Subject: [PATCH 33/62] eliminated the use of ai_agent_services --- server/app/api/ai_agent_viewset.py | 18 ++++++++++++++---- server/app/core/services/ai_agent_services.py | 18 ------------------ 2 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 server/app/core/services/ai_agent_services.py diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py index 7856796..e38e181 100644 --- a/server/app/api/ai_agent_viewset.py +++ b/server/app/api/ai_agent_viewset.py @@ -1,10 +1,11 @@ from bson import ObjectId -from flask import Response, jsonify, request, stream_with_context +from flask import Config, Response, jsonify, request, stream_with_context from flask_classful import FlaskView, route +from server.app.core.services.llm_response_service import LLMResponseService + from ..auth.decorators import login_required from ..config import Logging -from ..core.services.ai_agent_services import generate_conversation_service from ..models import AIAgent, AIAgentVariant, Project from ..serializers import AIAgentSerializer, AIAgentVariantSerializer @@ -113,15 +114,24 @@ def generate_conversation(self): config = data.get("config") prompt = data.get("prompt") stream = data.get("stream", True) + logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") + + # get model, max_tokens, temperature from config + model = config.get("model") + max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS + temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE + llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) + + # generate conversation with llm_service if stream: logger.info(f"Streaming conversation for prompt: {prompt}") return Response( - stream_with_context(generate_conversation_service(config, prompt, stream)), + stream_with_context(llm_service.get_streaming_response()), content_type="text/event-stream", ) else: logger.info(f"Generating conversation for prompt: {prompt} without stream") - return jsonify(generate_conversation_service(config, prompt, stream)), 200 + return jsonify(llm_service.get_response()), 200 except Exception as e: logger.error(f"Error generating conversation: {str(e)}") return jsonify({"error": str(e)}), 500 diff --git a/server/app/core/services/ai_agent_services.py b/server/app/core/services/ai_agent_services.py deleted file mode 100644 index 5baae01..0000000 --- a/server/app/core/services/ai_agent_services.py +++ /dev/null @@ -1,18 +0,0 @@ -from ...config import Config, Logging -from ..services.llm_response_service import LLMResponseService - -logger = Logging.get_logger(__name__) - - -def generate_conversation_service(config: dict, prompt: list[dict], stream=False): - logger.info(f"Generating conversion for prompt: {prompt} and config: {config} with stream: {stream}") - model = config.get("model") - max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS - temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE - llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) - - logger.info(f"Generated conversion for prompt: {prompt} and config: {config} with stream: {stream}") - if stream: - return llm_service.get_streaming_response() - else: - return llm_service.get_response() From 2b06f02f61d6cbc1bd3329bfffaf5eab62a2845f Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 04:17:09 +0530 Subject: [PATCH 34/62] minor change --- server/app/api/ai_agent_viewset.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py index e38e181..4394c1b 100644 --- a/server/app/api/ai_agent_viewset.py +++ b/server/app/api/ai_agent_viewset.py @@ -2,10 +2,9 @@ from flask import Config, Response, jsonify, request, stream_with_context from flask_classful import FlaskView, route -from server.app.core.services.llm_response_service import LLMResponseService - from ..auth.decorators import login_required from ..config import Logging +from ..core.services.llm_response_service import LLMResponseService from ..models import AIAgent, AIAgentVariant, Project from ..serializers import AIAgentSerializer, AIAgentVariantSerializer From d65de0466f70abdd0c871622f2f3f7c093debfde Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 04:30:45 +0530 Subject: [PATCH 35/62] made improvements to evaluator service --- server/app/api/evaluation_viewset.py | 8 ++- .../app/core/services/evaluation_services.py | 55 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/server/app/api/evaluation_viewset.py b/server/app/api/evaluation_viewset.py index 51ba056..0bf7aa2 100644 --- a/server/app/api/evaluation_viewset.py +++ b/server/app/api/evaluation_viewset.py @@ -2,13 +2,13 @@ from flask import jsonify, request from flask_classful import FlaskView -from .. import ( +from ..config import Logging +from ..core.services.evaluation_services import ( EmbeddingEvaluationStrategy, Evaluator, FuzzyEvaluationStrategy, LLMEvaluationStrategy, ) -from ..config import Logging from ..models import Project logger = Logging.get_logger(__name__) @@ -36,8 +36,10 @@ def post_index(self): evaluator_config = agent.agent_config or {} strategy_type = evaluator_config.get("strategy", "llm").lower() if strategy_type == "llm": + evaluator_instructions = agent.agent_config.get("instructions") evaluator_strategy = LLMEvaluationStrategy( - model=evaluator_config.get("model", "default-model"), config=evaluator_config + config=evaluator_config, + instructions=evaluator_instructions, ) elif strategy_type == "embedding": embedding_model = evaluator_config.get("embedding_model") diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index e82d1dd..d601aa7 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod import numpy as np +from flask import Config from fuzzywuzzy import fuzz from server.app.core.services.llm_response_service import LLMResponseService @@ -25,15 +26,18 @@ class LLMEvaluationStrategy(EvaluationStrategy): Strategy using an LLM for evaluation """ - def __init__(self, model: str, config: dict): - self.model = model + def __init__(self, config: dict, instructions: str): self.config = config + self.model = config.get("model", "gpt-4o") + self.max_tokens = config.get("max_tokens", Config.DEFAULT_MAX_TOKENS) + self.temperature = config.get("temperature", Config.DEFAULT_TEMPERATURE) + self.instructions = instructions - def _get_prompt(self, input_text: str, output_text: str, instructions: str): + def __get_prompt(self, input_text: str, output_text: str): prompt = "" - if instructions: + if self.instructions: prompt = f"Evaluate the following output given the input and instructions:\n" - prompt += f"Instructions: {instructions}\n" + prompt += f"Instructions: {self.instructions}\n" prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" else: prompt = f"Note: The score should be between 0 and 100. The higher the score, the better the output." @@ -41,13 +45,24 @@ def _get_prompt(self, input_text: str, output_text: str, instructions: str): prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" return prompt - def _validate_response_conformity(self, response: str, instructions: str): - if instructions: - prompt = f"These are the instructions: {instructions}\n" + def __validate_response_conformity_with_instructions(self, response: str): + if self.instructions: + prompt = f"These are the instructions: {self.instructions}\n" prompt += f"This is the Evaluator's response: {response}\n" prompt += f"Check if the response is consistent with the instructions. If it is, return 'True'. If it is not, return 'False'." - llm_response_service = LLMResponseService(model=self.model, prompt=[{"role": "user", "content": prompt}]) + + # initialize LLMResponseService + llm_response_service = LLMResponseService( + model=self.model, + prompt=[{"role": "user", "content": prompt}], + max_tokens=self.max_tokens, + temperature=self.temperature, + ) + + # get response response = llm_response_service.get_response() + + # validate response if response == "True": logger.info(f"Response is consistent with the instructions. Response: {response}") return True @@ -63,16 +78,28 @@ def _validate_response_conformity(self, response: str, instructions: str): logger.error(f"Response is not between 0 and 100. Response: {response}") return False - def evaluate(self, input_text: str, output_text: str, instructions: str, max_retries: int = 3): - prompt = self._get_prompt(input_text, output_text, instructions) - llm_response_service = LLMResponseService(model=self.model, prompt=[{"role": "user", "content": prompt}]) + def evaluate(self, input_text: str, output_text: str, max_retries: int = 3): + prompt = self._get_prompt(input_text, output_text) + + # unpack config and initialize LLMResponseService + llm_response_service = LLMResponseService( + model=self.model, + prompt=[{"role": "user", "content": prompt}], + max_tokens=self.max_tokens, + temperature=self.temperature, + ) + + # get response response = llm_response_service.get_response() - validation_passed = self._validate_response_conformity(response, instructions) + + # validate response + validation_passed = self._validate_response_conformity_with_instructions(response) + # If validation failed, re-try again if not validation_passed: # re-try again if max_retries > 0: - return self.evaluate(input_text, output_text, instructions, max_retries - 1) + return self.evaluate(input_text, output_text, max_retries - 1) else: logger.error(f"Response is not consistent with the instructions. Response: {response}") return False From cbca4a9bcd27e7a5a841ebfbaa732b7b4dd50d2a Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 05:00:53 +0530 Subject: [PATCH 36/62] bug fixes --- server/app/__init__.py | 4 ++-- server/app/api/ai_agent_viewset.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index 48eb2de..58f6a1d 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -23,8 +23,8 @@ def register_blueprints(app): api_bp = Blueprint("api", __name__, url_prefix="/api") # Register viewsets with their route bases. - AIAgentViewSet.register(api_bp) - AIAgentVariantViewSet.register(api_bp) + AIAgentViewSet.register(api_bp, route_base="//agents") + AIAgentVariantViewSet.register(api_bp, route_base="//agents//variants") AuthViewSet.register(api_bp) ConfigViewSet.register(api_bp) DatasetViewSet.register(api_bp) diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py index 4394c1b..608b65a 100644 --- a/server/app/api/ai_agent_viewset.py +++ b/server/app/api/ai_agent_viewset.py @@ -18,7 +18,6 @@ class AIAgentViewSet(FlaskView): """ decorators = [login_required] - route_prefix = "//agents" def index(self, project_id): """GET //agents -> List agents.""" @@ -143,7 +142,6 @@ class AIAgentVariantViewSet(FlaskView): """ decorators = [login_required] - route_prefix = "//agents//variants" def index(self, project_id, agent_id): """GET -> List variants for an agent.""" From 932c745dfdaf0ced7a4de1d8dfbdbeb9e4126e2c Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 05:28:58 +0530 Subject: [PATCH 37/62] fixed routes --- server/app/__init__.py | 4 +-- server/app/api/ai_agent_viewset.py | 39 +++++++++++++++++----------- server/app/api/auth_viewset.py | 2 +- server/app/api/dataset_viewset.py | 29 +++++++++++---------- server/app/api/evaluation_viewset.py | 2 +- server/app/api/main_viewset.py | 2 +- server/app/api/project_viewset.py | 24 ++++++++--------- 7 files changed, 57 insertions(+), 45 deletions(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index 58f6a1d..48eb2de 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -23,8 +23,8 @@ def register_blueprints(app): api_bp = Blueprint("api", __name__, url_prefix="/api") # Register viewsets with their route bases. - AIAgentViewSet.register(api_bp, route_base="//agents") - AIAgentVariantViewSet.register(api_bp, route_base="//agents//variants") + AIAgentViewSet.register(api_bp) + AIAgentVariantViewSet.register(api_bp) AuthViewSet.register(api_bp) ConfigViewSet.register(api_bp) DatasetViewSet.register(api_bp) diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py index 608b65a..4c1701f 100644 --- a/server/app/api/ai_agent_viewset.py +++ b/server/app/api/ai_agent_viewset.py @@ -18,17 +18,18 @@ class AIAgentViewSet(FlaskView): """ decorators = [login_required] + route_base = "//agents" - def index(self, project_id): - """GET //agents -> List agents.""" + def index(self, id): + """GET //agents -> List agents.""" try: - project = Project.objects.get(id=ObjectId(project_id)) + project = Project.objects.get(id=ObjectId(id)) serialized_agents = AIAgentSerializer(many=True).dump(project.ai_agents) - logger.info(f"Successfully fetched AI agents for project: {project_id}") + logger.info(f"Successfully fetched AI agents for project: {id}") return ( jsonify( { - "message": f"Successfully fetched AI agents for project: {project_id}", + "message": f"Successfully fetched AI agents for project: {id}", "data": serialized_agents, } ), @@ -38,11 +39,11 @@ def index(self, project_id): logger.error(f"Error fetching AI agents: {str(e)}") return jsonify({"error": str(e)}), 500 - def post(self, project_id): - """POST //agents -> Create a new agent.""" + def post(self, id): + """POST //agents -> Create a new agent.""" try: data = request.json - project = Project.objects.get(id=ObjectId(project_id)) + project = Project.objects.get(id=ObjectId(id)) new_agent = AIAgent( name=data["name"], agent_config=data["config"], @@ -58,7 +59,8 @@ def post(self, project_id): logger.error(f"Error creating AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 - def get(self, project_id, agent_id): + @route("/", methods=["GET"]) + def get_agent(self, project_id, agent_id): """GET //agents/ -> Retrieve a single agent.""" try: project = Project.objects.get(id=ObjectId(project_id)) @@ -70,7 +72,8 @@ def get(self, project_id, agent_id): logger.error(f"Error retrieving AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self, project_id, agent_id): + @route("/", methods=["PUT"], endpoint="update_agent") + def update_agent(self, project_id, agent_id): """PUT //agents/ -> Update an agent.""" try: data = request.json @@ -89,7 +92,8 @@ def put(self, project_id, agent_id): logger.error(f"Error updating AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self, project_id, agent_id): + @route("/", methods=["DELETE"], endpoint="delete_agent") + def delete_agent(self, project_id, agent_id): """DELETE //agents/ -> Remove an agent.""" try: project = Project.objects.get(id=ObjectId(project_id)) @@ -142,8 +146,10 @@ class AIAgentVariantViewSet(FlaskView): """ decorators = [login_required] + route_base = "//agents//variants" - def index(self, project_id, agent_id): + @route("/", methods=["GET"], endpoint="list_variants") + def list_variants(self, project_id, agent_id): """GET -> List variants for an agent.""" try: project = Project.objects.get(id=ObjectId(project_id)) @@ -157,7 +163,8 @@ def index(self, project_id, agent_id): logger.error(f"Error fetching variants: {str(e)}") return jsonify({"error": str(e)}), 500 - def post(self, project_id, agent_id): + @route("/", methods=["POST"], endpoint="create_variant") + def create_variant(self, project_id, agent_id): """POST -> Create a new variant for an agent.""" try: data = request.json @@ -178,7 +185,8 @@ def post(self, project_id, agent_id): logger.error(f"Error creating variant: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self, project_id, agent_id, variant_id): + @route("/", methods=["PUT"], endpoint="update_variant") + def update_variant(self, project_id, agent_id, variant_id): """PUT -> Update an agent variant.""" try: data = request.json @@ -198,7 +206,8 @@ def put(self, project_id, agent_id, variant_id): logger.error(f"Error updating variant: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self, project_id, agent_id, variant_id): + @route("/", methods=["DELETE"], endpoint="delete_variant") + def delete_variant(self, project_id, agent_id, variant_id): """DELETE -> Remove an agent variant.""" try: project = Project.objects.get(id=ObjectId(project_id)) diff --git a/server/app/api/auth_viewset.py b/server/app/api/auth_viewset.py index 140a6c6..31c8996 100644 --- a/server/app/api/auth_viewset.py +++ b/server/app/api/auth_viewset.py @@ -9,7 +9,7 @@ class AuthViewSet(FlaskView): - route_prefix = "/auth" + route_base = "/auth" def post_verify(self): """POST /auth/verify -> Verify Firebase token and create session.""" diff --git a/server/app/api/dataset_viewset.py b/server/app/api/dataset_viewset.py index 853898b..0847647 100644 --- a/server/app/api/dataset_viewset.py +++ b/server/app/api/dataset_viewset.py @@ -12,11 +12,11 @@ class DatasetViewSet(FlaskView): decorators = [login_required] - route_prefix = "/datasets" + route_base = "//datasets" - @route("/project/", methods=["GET"]) + @route("/", methods=["GET"], endpoint="index_by_project") def index_by_project(self, project_id): - """GET /datasets/project/ -> Get datasets for a project.""" + """GET //datasets -> Get datasets for a project.""" try: project = Project.objects.get(id=ObjectId(project_id)) datasets = Dataset.objects.filter(project=project, is_alive=True) @@ -27,11 +27,12 @@ def index_by_project(self, project_id): logger.error(f"Error fetching datasets: {str(e)}") return jsonify({"message": str(e)}), 500 - def post(self): - """POST /datasets -> Create a new dataset.""" + @route("/", methods=["POST"], endpoint="create_dataset") + def create_dataset(self, project_id): + """POST //datasets -> Create a new dataset.""" try: data = request.json - project = Project.objects.get(id=ObjectId(data["project_id"])) + project = Project.objects.get(id=ObjectId(project_id)) dataset = Dataset( project=project, name=data["name"], @@ -45,8 +46,9 @@ def post(self): logger.error(f"Error creating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - def put(self, dataset_id): - """PUT /datasets/ -> Update a dataset.""" + @route("/", methods=["PUT"], endpoint="update_dataset") + def update_dataset(self, dataset_id): + """PUT //datasets/ -> Update a dataset.""" try: data = request.json dataset = Dataset.objects.get(id=dataset_id) @@ -61,8 +63,9 @@ def put(self, dataset_id): logger.error(f"Error updating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - def delete(self, dataset_id): - """DELETE /datasets/ -> Delete a dataset.""" + @route("/", methods=["DELETE"], endpoint="delete_dataset") + def delete_dataset(self, dataset_id): + """DELETE //datasets/ -> Delete a dataset.""" try: dataset = Dataset.objects.get(id=dataset_id) if not dataset: @@ -76,7 +79,7 @@ def delete(self, dataset_id): @route("//samples", methods=["POST"]) def add_samples(self, dataset_id): - """POST /datasets//samples -> Add a sample.""" + """POST //datasets//samples -> Add a sample.""" try: data = request.json dataset = Dataset.objects.get(id=dataset_id) @@ -95,7 +98,7 @@ def add_samples(self, dataset_id): @route("//samples", methods=["DELETE"]) def remove_samples(self, dataset_id): - """DELETE /datasets//samples -> Remove a sample.""" + """DELETE //datasets//samples -> Remove a sample.""" try: data = request.json dataset = Dataset.objects.get(id=dataset_id) @@ -115,7 +118,7 @@ def remove_samples(self, dataset_id): @route("//samples", methods=["PUT"]) def update_sample(self, dataset_id): - """PUT /datasets//samples -> Update a sample.""" + """PUT //datasets//samples -> Update a sample.""" try: data = request.json dataset = Dataset.objects.get(id=dataset_id) diff --git a/server/app/api/evaluation_viewset.py b/server/app/api/evaluation_viewset.py index 0bf7aa2..3fc0d21 100644 --- a/server/app/api/evaluation_viewset.py +++ b/server/app/api/evaluation_viewset.py @@ -15,7 +15,7 @@ class EvaluationViewSet(FlaskView): - route_prefix = "/evaluation" + route_base = "/evaluation" def post_index(self): """POST /evaluation -> Evaluate an agent's output against input.""" diff --git a/server/app/api/main_viewset.py b/server/app/api/main_viewset.py index 759ec19..f73b01c 100644 --- a/server/app/api/main_viewset.py +++ b/server/app/api/main_viewset.py @@ -3,7 +3,7 @@ class MainViewSet(FlaskView): - route_prefix = "/" + route_base = "/" def index(self): """GET / -> Homepage; redirects to login if user not in session.""" diff --git a/server/app/api/project_viewset.py b/server/app/api/project_viewset.py index 293b22a..e6a5ea2 100644 --- a/server/app/api/project_viewset.py +++ b/server/app/api/project_viewset.py @@ -12,7 +12,7 @@ class ProjectViewSet(FlaskView): decorators = [login_required] - route_prefix = "/projects" + route_base = "/projects" def index(self): """GET /projects -> List projects for the current user.""" @@ -32,15 +32,15 @@ def index(self): logger.error(f"Error fetching projects: {str(e)}") return jsonify({"error": str(e)}), 500 - def get(self, project_id): - """GET /projects/ -> Fetch a single project.""" + def get(self, id): + """GET /projects/ -> Get a project.""" try: - project = Project.objects.get(id=ObjectId(project_id)) + project = Project.objects.get(id=ObjectId(id)) serialized_project = ProjectSerializer().dump(project) - logger.info(f"Fetched project: {project_id}") + logger.info(f"Fetched project: {id}") return jsonify( { - "message": f"Successfully fetched project: {project_id}", + "message": f"Successfully fetched project: {id}", "data": serialized_project, } ) @@ -67,11 +67,11 @@ def post(self): logger.error(f"Error creating project: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self, project_id): - """PUT /projects/ -> Update a project.""" + def put(self, id): + """PUT /projects/ -> Update a project.""" try: data = request.json - project = Project.objects.get(id=ObjectId(project_id)) + project = Project.objects.get(id=ObjectId(id)) project.name = data.get("name", project.name) project.description = data.get("description", project.description) project.save() @@ -81,10 +81,10 @@ def put(self, project_id): logger.error(f"Error updating project: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self, project_id): - """DELETE /projects/ -> Delete a project.""" + def delete(self, id): + """DELETE /projects/ -> Delete a project.""" try: - project = Project.objects.get(id=ObjectId(project_id)) + project = Project.objects.get(id=ObjectId(id)) project.delete() logger.info("Project deleted successfully") return jsonify({"message": "Project deleted successfully"}), 200 From 8049e7910ef736938f580dd22d108e637a57b558 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 06:08:33 +0530 Subject: [PATCH 38/62] fixed url paths --- server/app/__init__.py | 51 ++-- server/app/api/agent_variant_view.py | 102 ++++++++ server/app/api/ai_agent_view.py | 139 +++++++++++ server/app/api/ai_agent_viewset.py | 226 ------------------ .../app/api/{auth_viewset.py => auth_view.py} | 27 ++- server/app/api/config_view.py | 14 ++ server/app/api/config_viewset.py | 12 - .../{dataset_viewset.py => dataset_view.py} | 58 +++-- ...aluation_viewset.py => evaluation_view.py} | 23 +- server/app/api/main_view.py | 21 ++ server/app/api/main_viewset.py | 18 -- .../{project_viewset.py => project_view.py} | 70 +++--- 12 files changed, 400 insertions(+), 361 deletions(-) create mode 100644 server/app/api/agent_variant_view.py create mode 100644 server/app/api/ai_agent_view.py delete mode 100644 server/app/api/ai_agent_viewset.py rename server/app/api/{auth_viewset.py => auth_view.py} (73%) create mode 100644 server/app/api/config_view.py delete mode 100644 server/app/api/config_viewset.py rename server/app/api/{dataset_viewset.py => dataset_view.py} (77%) rename server/app/api/{evaluation_viewset.py => evaluation_view.py} (81%) create mode 100644 server/app/api/main_view.py delete mode 100644 server/app/api/main_viewset.py rename server/app/api/{project_viewset.py => project_view.py} (63%) diff --git a/server/app/__init__.py b/server/app/__init__.py index 48eb2de..5dc303a 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -5,34 +5,30 @@ from flask import Blueprint, Flask from flask_cors import CORS -from .api.ai_agent_viewset import AIAgentVariantViewSet, AIAgentViewSet -from .api.auth_viewset import AuthViewSet -from .api.config_viewset import ConfigViewSet -from .api.dataset_viewset import DatasetViewSet -from .api.evaluation_viewset import EvaluationViewSet -from .api.main_viewset import MainViewSet -from .api.project_viewset import ProjectViewSet +from .api.agent_variant_view import variant_bp +from .api.ai_agent_view import ai_agent_bp +from .api.auth_view import auth_bp +from .api.config_view import config_bp +from .api.dataset_view import dataset_bp +from .api.evaluation_view import evaluation_bp +from .api.main_view import main_bp +from .api.project_view import project_bp from .config import Config, Logging # Initialize logging once Logging.initialize(log_level=logging.INFO, log_file="application.log") -def register_blueprints(app): - # Create an API blueprint +def register_api_blueprints(app): api_bp = Blueprint("api", __name__, url_prefix="/api") - - # Register viewsets with their route bases. - AIAgentViewSet.register(api_bp) - AIAgentVariantViewSet.register(api_bp) - AuthViewSet.register(api_bp) - ConfigViewSet.register(api_bp) - DatasetViewSet.register(api_bp) - EvaluationViewSet.register(api_bp) - MainViewSet.register(api_bp) - ProjectViewSet.register(api_bp) - - # Register the API blueprint + api_bp.register_blueprint(config_bp) + api_bp.register_blueprint(main_bp) + api_bp.register_blueprint(auth_bp) + api_bp.register_blueprint(project_bp) + api_bp.register_blueprint(evaluation_bp) + api_bp.register_blueprint(dataset_bp) + api_bp.register_blueprint(ai_agent_bp) + api_bp.register_blueprint(variant_bp) app.register_blueprint(api_bp) @@ -41,12 +37,23 @@ def create_app(config_class=Config): Create a Flask app with the given configuration. """ app = Flask(__name__, static_folder="../../src/static", template_folder="../../src/templates") + + # mongoengine mongoengine.connect( host=config_class.MONGO_URI, ssl=True, tlscafile=certifi.where(), ) + + # config app.config.from_object(config_class) + + # make url_map not strict + app.url_map.strict_slashes = False + + # cors CORS(app) - register_blueprints(app) + + # register api blueprints + register_api_blueprints(app) return app diff --git a/server/app/api/agent_variant_view.py b/server/app/api/agent_variant_view.py new file mode 100644 index 0000000..39b0822 --- /dev/null +++ b/server/app/api/agent_variant_view.py @@ -0,0 +1,102 @@ +from bson import ObjectId +from flask import Blueprint, jsonify, request +from flask.views import MethodView + +from ..auth.decorators import login_required +from ..config import Logging +from ..models import AIAgentVariant, Project +from ..serializers import AIAgentVariantSerializer + +logger = Logging.get_logger(__name__) +agent_variant_bp = Blueprint("agent_variant", __name__, url_prefix="/variants") + + +class VariantListAPI(MethodView): + decorators = [login_required] + + def get(self): + try: + project_id = request.args.get("project_id") + agent_id = request.args.get("agent_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + serialized = AIAgentVariantSerializer(many=True).dump(agent.variants) + logger.info("Successfully fetched variants") + return jsonify({"message": "Successfully fetched variants", "data": serialized}), 200 + except Exception as e: + logger.error(f"Error fetching variants: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def post(self): + try: + data = request.json + project_id = data.get("project_id") + agent_id = data.get("agent_id") + variant_name = data.get("name") + variables = data.get("variables") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + if not set(variables.keys()).issubset(set(agent.included_variables)): + return jsonify({"error": "Variables not included in the agent"}), 400 + new_variant = AIAgentVariant(name=variant_name, variables=variables, is_alive=True) + agent.variants.append(new_variant) + project.save() + logger.info("Variant created successfully") + return jsonify({"message": "Variant created successfully"}), 201 + except Exception as e: + logger.error(f"Error creating variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +class VariantDetailAPI(MethodView): + decorators = [login_required] + + def put(self): + try: + data = request.json + project_id = data.get("project_id") + agent_id = data.get("agent_id") + variant_id = data.get("variant_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + variant.name = data.get("name", variant.name) + variant.variables = data.get("variables", variant.variables) + project.save() + logger.info("Variant updated successfully") + return jsonify({"message": "Variant updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def delete(self): + try: + project_id = request.args.get("project_id") + agent_id = request.args.get("agent_id") + variant_id = request.args.get("variant_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + agent.variants.remove(variant) + project.save() + logger.info("Variant deleted successfully") + return jsonify({"message": "Variant deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +agent_variant_bp.add_url_rule("/", view_func=VariantListAPI.as_view("variant_list_api"), methods=["GET", "POST"]) +agent_variant_bp.add_url_rule("/", view_func=VariantDetailAPI.as_view("variant_detail_api"), methods=["PUT", "DELETE"]) diff --git a/server/app/api/ai_agent_view.py b/server/app/api/ai_agent_view.py new file mode 100644 index 0000000..0b10899 --- /dev/null +++ b/server/app/api/ai_agent_view.py @@ -0,0 +1,139 @@ +from bson import ObjectId +from flask import Blueprint, Response, jsonify, request, stream_with_context +from flask.views import MethodView + +from ..auth.decorators import login_required +from ..config import Config, Logging +from ..core.services.llm_response_service import LLMResponseService +from ..models import AIAgent, Project +from ..serializers import AIAgentSerializer + +logger = Logging.get_logger(__name__) +ai_agent_bp = Blueprint("ai_agent", __name__, url_prefix="/agents") + + +class AIAgentAPI(MethodView): + decorators = [login_required] + + def get(self): + try: + project_id = request.args.get("project_id") + project = Project.objects.get(id=ObjectId(project_id)) + agents = AIAgentSerializer(many=True).dump(project.ai_agents) + logger.info(f"Successfully fetched AI agents for project: {project_id}") + return ( + jsonify({"message": f"Successfully fetched AI agents for project: {project_id}", "data": agents}), + 200, + ) + except Exception as e: + logger.error(f"Error fetching AI agents: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def post(self): + try: + data = request.json + project_id = data.get("project_id") + project = Project.objects.get(id=ObjectId(project_id)) + new_agent = AIAgent( + name=data["name"], + agent_config=data["config"], + prompt=data["prompt"], + included_variables=data["included_variables"], + is_alive=True, + ) + project.ai_agents.append(new_agent) + project.save() + logger.info("AI Agent created successfully") + return jsonify({"message": "AI Agent created successfully"}), 201 + except Exception as e: + logger.error(f"Error creating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +class AIAgentDetailAPI(MethodView): + decorators = [login_required] + + def get(self): + try: + project_id = request.args.get("project_id") + agent_id = request.args.get("agent_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if agent: + return jsonify(AIAgentSerializer().dump(agent)), 200 + return jsonify({"error": "Agent not found"}), 404 + except Exception as e: + logger.error(f"Error retrieving AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self): + try: + data = request.json + project_id = data.get("project_id") + agent_id = data.get("agent_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + agent.name = data.get("name", agent.name) + agent.agent_config = data.get("config", agent.agent_config) + agent.prompt = data.get("prompt", agent.prompt) + agent.included_variables = data.get("included_variables", agent.included_variables) + project.save() + logger.info("AI Agent updated successfully") + return jsonify({"message": "AI Agent updated successfully"}), 200 + except Exception as e: + logger.error(f"Error updating AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def delete(self): + try: + project_id = request.args.get("project_id") + agent_id = request.args.get("agent_id") + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + project.ai_agents.remove(agent) + project.save() + logger.info("AI Agent deleted successfully") + return jsonify({"message": "AI Agent deleted successfully"}), 200 + except Exception as e: + logger.error(f"Error deleting AI agent: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +class ConversationAPI(MethodView): + decorators = [login_required] + + def post(self): + try: + data = request.json + config = data.get("config") + prompt = data.get("prompt") + stream_enabled = data.get("stream", True) + logger.info( + f"Generating conversation for prompt: {prompt} with config: {config} and stream: {stream_enabled}" + ) + model = config.get("model") + max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS + temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE + llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) + if stream_enabled: + logger.info(f"Streaming conversation for prompt: {prompt}") + return Response( + stream_with_context(llm_service.get_streaming_response()), content_type="text/event-stream" + ) + else: + logger.info(f"Generating conversation for prompt: {prompt} without stream") + return jsonify(llm_service.get_response()), 200 + except Exception as e: + logger.error(f"Error generating conversation: {str(e)}") + return jsonify({"error": str(e)}), 500 + + +ai_agent_bp.add_url_rule("/", view_func=AIAgentAPI.as_view("ai_agent_api"), methods=["GET", "POST"]) +ai_agent_bp.add_url_rule( + "/detail", view_func=AIAgentDetailAPI.as_view("ai_agent_detail_api"), methods=["GET", "PUT", "DELETE"] +) +ai_agent_bp.add_url_rule("/conversation", view_func=ConversationAPI.as_view("conversation_api"), methods=["POST"]) diff --git a/server/app/api/ai_agent_viewset.py b/server/app/api/ai_agent_viewset.py deleted file mode 100644 index 4c1701f..0000000 --- a/server/app/api/ai_agent_viewset.py +++ /dev/null @@ -1,226 +0,0 @@ -from bson import ObjectId -from flask import Config, Response, jsonify, request, stream_with_context -from flask_classful import FlaskView, route - -from ..auth.decorators import login_required -from ..config import Logging -from ..core.services.llm_response_service import LLMResponseService -from ..models import AIAgent, AIAgentVariant, Project -from ..serializers import AIAgentSerializer, AIAgentVariantSerializer - -logger = Logging.get_logger(__name__) - - -class AIAgentViewSet(FlaskView): - """ - ViewSet for AI Agents. - URL Prefix: //agents - """ - - decorators = [login_required] - route_base = "//agents" - - def index(self, id): - """GET //agents -> List agents.""" - try: - project = Project.objects.get(id=ObjectId(id)) - serialized_agents = AIAgentSerializer(many=True).dump(project.ai_agents) - logger.info(f"Successfully fetched AI agents for project: {id}") - return ( - jsonify( - { - "message": f"Successfully fetched AI agents for project: {id}", - "data": serialized_agents, - } - ), - 200, - ) - except Exception as e: - logger.error(f"Error fetching AI agents: {str(e)}") - return jsonify({"error": str(e)}), 500 - - def post(self, id): - """POST //agents -> Create a new agent.""" - try: - data = request.json - project = Project.objects.get(id=ObjectId(id)) - new_agent = AIAgent( - name=data["name"], - agent_config=data["config"], - prompt=data["prompt"], - included_variables=data["included_variables"], - is_alive=True, - ) - project.ai_agents.append(new_agent) - project.save() - logger.info("AI Agent created successfully") - return jsonify({"message": "AI Agent created successfully"}), 201 - except Exception as e: - logger.error(f"Error creating AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["GET"]) - def get_agent(self, project_id, agent_id): - """GET //agents/ -> Retrieve a single agent.""" - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if agent: - return jsonify(AIAgentSerializer().dump(agent)), 200 - return jsonify({"error": "Agent not found"}), 404 - except Exception as e: - logger.error(f"Error retrieving AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["PUT"], endpoint="update_agent") - def update_agent(self, project_id, agent_id): - """PUT //agents/ -> Update an agent.""" - try: - data = request.json - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - agent.name = data.get("name", agent.name) - agent.agent_config = data.get("config", agent.agent_config) - agent.prompt = data.get("prompt", agent.prompt) - agent.included_variables = data.get("included_variables", agent.included_variables) - project.save() - logger.info("AI Agent updated successfully") - return jsonify({"message": "AI Agent updated successfully"}), 200 - except Exception as e: - logger.error(f"Error updating AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["DELETE"], endpoint="delete_agent") - def delete_agent(self, project_id, agent_id): - """DELETE //agents/ -> Remove an agent.""" - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - project.ai_agents.remove(agent) - project.save() - logger.info("AI Agent deleted successfully") - return jsonify({"message": "AI Agent deleted successfully"}), 200 - except Exception as e: - logger.error(f"Error deleting AI agent: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/conversation", methods=["POST"], endpoint="conversation") - def generate_conversation(self): - """POST /agents/conversation -> Generate a conversation.""" - try: - data = request.json - config = data.get("config") - prompt = data.get("prompt") - stream = data.get("stream", True) - logger.info(f"Generating conversation for prompt: {prompt} and config: {config} with stream: {stream}") - - # get model, max_tokens, temperature from config - model = config.get("model") - max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS - temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE - llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) - - # generate conversation with llm_service - if stream: - logger.info(f"Streaming conversation for prompt: {prompt}") - return Response( - stream_with_context(llm_service.get_streaming_response()), - content_type="text/event-stream", - ) - else: - logger.info(f"Generating conversation for prompt: {prompt} without stream") - return jsonify(llm_service.get_response()), 200 - except Exception as e: - logger.error(f"Error generating conversation: {str(e)}") - return jsonify({"error": str(e)}), 500 - - -class AIAgentVariantViewSet(FlaskView): - """ - ViewSet for AI Agent Variants. - URL Prefix: //agents//variants - """ - - decorators = [login_required] - route_base = "//agents//variants" - - @route("/", methods=["GET"], endpoint="list_variants") - def list_variants(self, project_id, agent_id): - """GET -> List variants for an agent.""" - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - serialized_variants = AIAgentVariantSerializer(many=True).dump(agent.variants) - logger.info("Successfully fetched variants") - return jsonify({"message": "Successfully fetched variants", "data": serialized_variants}), 200 - except Exception as e: - logger.error(f"Error fetching variants: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["POST"], endpoint="create_variant") - def create_variant(self, project_id, agent_id): - """POST -> Create a new variant for an agent.""" - try: - data = request.json - variant_name = data.get("name") - variables = data.get("variables") - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - if not set(variables.keys()).issubset(set(agent.included_variables)): - return jsonify({"error": "Variables not included in the agent"}), 400 - new_variant = AIAgentVariant(name=variant_name, variables=variables, is_alive=True) - agent.variants.append(new_variant) - project.save() - logger.info("Variant created successfully") - return jsonify({"message": "Variant created successfully"}), 201 - except Exception as e: - logger.error(f"Error creating variant: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["PUT"], endpoint="update_variant") - def update_variant(self, project_id, agent_id, variant_id): - """PUT -> Update an agent variant.""" - try: - data = request.json - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) - if not variant: - return jsonify({"error": "Variant not found"}), 404 - variant.name = data.get("name", variant.name) - variant.variables = data.get("variables", variant.variables) - project.save() - logger.info("Variant updated successfully") - return jsonify({"message": "Variant updated successfully"}), 200 - except Exception as e: - logger.error(f"Error updating variant: {str(e)}") - return jsonify({"error": str(e)}), 500 - - @route("/", methods=["DELETE"], endpoint="delete_variant") - def delete_variant(self, project_id, agent_id, variant_id): - """DELETE -> Remove an agent variant.""" - try: - project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) - if not agent: - return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) - if not variant: - return jsonify({"error": "Variant not found"}), 404 - agent.variants.remove(variant) - project.save() - logger.info("Variant deleted successfully") - return jsonify({"message": "Variant deleted successfully"}), 200 - except Exception as e: - logger.error(f"Error deleting variant: {str(e)}") - return jsonify({"error": str(e)}), 500 diff --git a/server/app/api/auth_viewset.py b/server/app/api/auth_view.py similarity index 73% rename from server/app/api/auth_viewset.py rename to server/app/api/auth_view.py index 31c8996..c957c60 100644 --- a/server/app/api/auth_viewset.py +++ b/server/app/api/auth_view.py @@ -1,18 +1,16 @@ -from flask import jsonify, request, session -from flask_classful import FlaskView +from flask import Blueprint, jsonify, request, session +from flask.views import MethodView from ..auth.handlers import AuthHandler from ..config import Logging from ..models import User logger = Logging.get_logger(__name__) +auth_bp = Blueprint("auth", __name__, url_prefix="/auth") -class AuthViewSet(FlaskView): - route_base = "/auth" - - def post_verify(self): - """POST /auth/verify -> Verify Firebase token and create session.""" +class VerifyAPI(MethodView): + def post(self): data = request.get_json() id_token = data.get("idToken") if not id_token: @@ -38,15 +36,22 @@ def post_verify(self): AuthHandler.create_session(decoded_token) return jsonify({"success": True, "user": session["user"]}) - def post_logout(self): - """POST /auth/logout -> Clear user session.""" + +class LogoutAPI(MethodView): + def post(self): logger.info(f"User logged out: {session.get('user', {}).get('email')}") AuthHandler.clear_session() return jsonify({"success": True}) - def get_session(self): - """GET /auth/session -> Retrieve current session info.""" + +class SessionAPI(MethodView): + def get(self): user = session.get("user") if user: return jsonify({"user": user}) return jsonify({"user": None}), 401 + + +auth_bp.add_url_rule("/verify", view_func=VerifyAPI.as_view("verify_api"), methods=["POST"]) +auth_bp.add_url_rule("/logout", view_func=LogoutAPI.as_view("logout_api"), methods=["POST"]) +auth_bp.add_url_rule("/session", view_func=SessionAPI.as_view("session_api"), methods=["GET"]) diff --git a/server/app/api/config_view.py b/server/app/api/config_view.py new file mode 100644 index 0000000..90cd79f --- /dev/null +++ b/server/app/api/config_view.py @@ -0,0 +1,14 @@ +from flask import Blueprint, jsonify +from flask.views import MethodView + +from ..utils.model_map import LLM_MODELS + + +class ConfigAPI(MethodView): + def get(self): + return jsonify(LLM_MODELS) + + +config_bp = Blueprint("config", __name__, url_prefix="/config") +config_view = ConfigAPI.as_view("config_api") +config_bp.add_url_rule("/models", view_func=config_view, methods=["GET"]) diff --git a/server/app/api/config_viewset.py b/server/app/api/config_viewset.py deleted file mode 100644 index ab9a10b..0000000 --- a/server/app/api/config_viewset.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask import jsonify -from flask_classful import FlaskView, route - -from ..utils.model_map import LLM_MODELS - - -class ConfigViewSet(FlaskView): - route_base = "/config" - - @route("/models", methods=["GET"]) - def get_models(self): - return jsonify(LLM_MODELS) diff --git a/server/app/api/dataset_viewset.py b/server/app/api/dataset_view.py similarity index 77% rename from server/app/api/dataset_viewset.py rename to server/app/api/dataset_view.py index 0847647..27d9464 100644 --- a/server/app/api/dataset_viewset.py +++ b/server/app/api/dataset_view.py @@ -1,6 +1,6 @@ from bson import ObjectId -from flask import jsonify, request -from flask_classful import FlaskView, route +from flask import Blueprint, jsonify, request +from flask.views import MethodView from ..auth.decorators import login_required from ..config import Logging @@ -8,16 +8,15 @@ from ..serializers import DatasetSerializer logger = Logging.get_logger(__name__) +dataset_bp = Blueprint("dataset", __name__, url_prefix="//datasets") -class DatasetViewSet(FlaskView): +class DatasetListAPI(MethodView): decorators = [login_required] - route_base = "//datasets" - @route("/", methods=["GET"], endpoint="index_by_project") - def index_by_project(self, project_id): - """GET //datasets -> Get datasets for a project.""" + def get(self): try: + project_id = request.args.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) datasets = Dataset.objects.filter(project=project, is_alive=True) serialized = DatasetSerializer(many=True).dump(datasets) @@ -27,11 +26,10 @@ def index_by_project(self, project_id): logger.error(f"Error fetching datasets: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("/", methods=["POST"], endpoint="create_dataset") - def create_dataset(self, project_id): - """POST //datasets -> Create a new dataset.""" + def post(self): try: data = request.json + project_id = data.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) dataset = Dataset( project=project, @@ -46,11 +44,14 @@ def create_dataset(self, project_id): logger.error(f"Error creating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("/", methods=["PUT"], endpoint="update_dataset") - def update_dataset(self, dataset_id): - """PUT //datasets/ -> Update a dataset.""" + +class DatasetDetailAPI(MethodView): + decorators = [login_required] + + def put(self): try: data = request.json + dataset_id = data.get("dataset_id") dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 @@ -63,10 +64,9 @@ def update_dataset(self, dataset_id): logger.error(f"Error updating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("/", methods=["DELETE"], endpoint="delete_dataset") - def delete_dataset(self, dataset_id): - """DELETE //datasets/ -> Delete a dataset.""" + def delete(self): try: + dataset_id = request.args.get("dataset_id") dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 @@ -77,11 +77,14 @@ def delete_dataset(self, dataset_id): logger.error(f"Error deleting dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("//samples", methods=["POST"]) - def add_samples(self, dataset_id): - """POST //datasets//samples -> Add a sample.""" + +class DatasetSampleAPI(MethodView): + decorators = [login_required] + + def post(self): try: data = request.json + dataset_id = data.get("dataset_id") dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 @@ -96,11 +99,10 @@ def add_samples(self, dataset_id): logger.error(f"Error adding sample: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("//samples", methods=["DELETE"]) - def remove_samples(self, dataset_id): - """DELETE //datasets//samples -> Remove a sample.""" + def delete(self): try: data = request.json + dataset_id = data.get("dataset_id") dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 @@ -116,11 +118,10 @@ def remove_samples(self, dataset_id): logger.error(f"Error removing sample: {str(e)}") return jsonify({"message": str(e)}), 500 - @route("//samples", methods=["PUT"]) - def update_sample(self, dataset_id): - """PUT //datasets//samples -> Update a sample.""" + def put(self): try: data = request.json + dataset_id = data.get("dataset_id") dataset = Dataset.objects.get(id=dataset_id) if not dataset: return jsonify({"message": "Dataset not found"}), 404 @@ -137,3 +138,10 @@ def update_sample(self, dataset_id): except Exception as e: logger.error(f"Error updating sample: {str(e)}") return jsonify({"message": str(e)}), 500 + + +dataset_bp.add_url_rule("/", view_func=DatasetListAPI.as_view("dataset_list_api"), methods=["GET", "POST"]) +dataset_bp.add_url_rule("/", view_func=DatasetDetailAPI.as_view("dataset_detail_api"), methods=["PUT", "DELETE"]) +dataset_bp.add_url_rule( + "/samples", view_func=DatasetSampleAPI.as_view("dataset_sample_api"), methods=["POST", "DELETE", "PUT"] +) diff --git a/server/app/api/evaluation_viewset.py b/server/app/api/evaluation_view.py similarity index 81% rename from server/app/api/evaluation_viewset.py rename to server/app/api/evaluation_view.py index 3fc0d21..5f80aad 100644 --- a/server/app/api/evaluation_viewset.py +++ b/server/app/api/evaluation_view.py @@ -1,6 +1,6 @@ from bson import ObjectId -from flask import jsonify, request -from flask_classful import FlaskView +from flask import Blueprint, jsonify, request +from flask.views import MethodView from ..config import Logging from ..core.services.evaluation_services import ( @@ -12,13 +12,11 @@ from ..models import Project logger = Logging.get_logger(__name__) +evaluation_bp = Blueprint("evaluation", __name__, url_prefix="/evaluation") -class EvaluationViewSet(FlaskView): - route_base = "/evaluation" - - def post_index(self): - """POST /evaluation -> Evaluate an agent's output against input.""" +class EvaluationAPI(MethodView): + def post(self): try: data = request.json project_id = data.get("project_id") @@ -27,6 +25,7 @@ def post_index(self): output_text = data.get("output_text") if not all([project_id, agent_id, input_text, output_text]): return jsonify({"error": "project_id, agent_id, input_text and output_text are required"}), 400 + project = Project.objects.get(id=ObjectId(project_id)) try: agent = project.ai_agents[int(agent_id)] @@ -36,11 +35,8 @@ def post_index(self): evaluator_config = agent.agent_config or {} strategy_type = evaluator_config.get("strategy", "llm").lower() if strategy_type == "llm": - evaluator_instructions = agent.agent_config.get("instructions") - evaluator_strategy = LLMEvaluationStrategy( - config=evaluator_config, - instructions=evaluator_instructions, - ) + instructions = evaluator_config.get("instructions") + evaluator_strategy = LLMEvaluationStrategy(config=evaluator_config, instructions=instructions) elif strategy_type == "embedding": embedding_model = evaluator_config.get("embedding_model") if not embedding_model: @@ -58,3 +54,6 @@ def post_index(self): except Exception as e: logger.error(f"Error during evaluation: {e}") return jsonify({"error": str(e)}), 500 + + +evaluation_bp.add_url_rule("/", view_func=EvaluationAPI.as_view("evaluation_api"), methods=["POST"]) diff --git a/server/app/api/main_view.py b/server/app/api/main_view.py new file mode 100644 index 0000000..ba68e99 --- /dev/null +++ b/server/app/api/main_view.py @@ -0,0 +1,21 @@ +from flask import Blueprint, redirect, render_template, session +from flask.views import MethodView + + +class HomeAPI(MethodView): + def get(self): + if "user" not in session: + return redirect("/login") + return render_template("index.html") + + +class LoginAPI(MethodView): + def get(self): + if "user" in session: + return redirect("/") + return render_template("login.html") + + +main_bp = Blueprint("main", __name__) +main_bp.add_url_rule("/", view_func=HomeAPI.as_view("home_api"), methods=["GET"]) +main_bp.add_url_rule("/login", view_func=LoginAPI.as_view("login_api"), methods=["GET"]) diff --git a/server/app/api/main_viewset.py b/server/app/api/main_viewset.py deleted file mode 100644 index f73b01c..0000000 --- a/server/app/api/main_viewset.py +++ /dev/null @@ -1,18 +0,0 @@ -from flask import redirect, render_template, session -from flask_classful import FlaskView - - -class MainViewSet(FlaskView): - route_base = "/" - - def index(self): - """GET / -> Homepage; redirects to login if user not in session.""" - if "user" not in session: - return redirect("/login") - return render_template("index.html") - - def login(self): - """GET /login -> Render login page; redirect if already logged in.""" - if "user" in session: - return redirect("/") - return render_template("login.html") diff --git a/server/app/api/project_viewset.py b/server/app/api/project_view.py similarity index 63% rename from server/app/api/project_viewset.py rename to server/app/api/project_view.py index e6a5ea2..bda632a 100644 --- a/server/app/api/project_viewset.py +++ b/server/app/api/project_view.py @@ -1,6 +1,6 @@ from bson import ObjectId -from flask import jsonify, request, session -from flask_classful import FlaskView +from flask import Blueprint, jsonify, request, session +from flask.views import MethodView from ..auth.decorators import login_required from ..config import Logging @@ -8,48 +8,27 @@ from ..serializers import ProjectSerializer logger = Logging.get_logger(__name__) +project_bp = Blueprint("project", __name__, url_prefix="/projects") -class ProjectViewSet(FlaskView): +class ProjectListAPI(MethodView): decorators = [login_required] - route_base = "/projects" - def index(self): - """GET /projects -> List projects for the current user.""" + def get(self): try: firebase_uid = session["user"]["uid"] user = User.objects.get(firebase_uid=firebase_uid) projects = Project.objects.filter(user=user, is_alive=True) - serialized_projects = ProjectSerializer(many=True).dump(projects) + serialized = ProjectSerializer(many=True).dump(projects) logger.info(f"Fetched projects for user: {firebase_uid}") return jsonify( - { - "message": f"Successfully fetched projects for user_id: {str(user.id)}", - "data": serialized_projects, - } + {"message": f"Successfully fetched projects for user_id: {str(user.id)}", "data": serialized} ) except Exception as e: logger.error(f"Error fetching projects: {str(e)}") return jsonify({"error": str(e)}), 500 - def get(self, id): - """GET /projects/ -> Get a project.""" - try: - project = Project.objects.get(id=ObjectId(id)) - serialized_project = ProjectSerializer().dump(project) - logger.info(f"Fetched project: {id}") - return jsonify( - { - "message": f"Successfully fetched project: {id}", - "data": serialized_project, - } - ) - except Exception as e: - logger.error(f"Error fetching project: {str(e)}") - return jsonify({"error": str(e)}), 500 - def post(self): - """POST /projects -> Create a new project.""" try: data = request.json firebase_uid = session["user"]["uid"] @@ -58,7 +37,7 @@ def post(self): user=user, name=data.get("name", "Default Project"), description=data.get("description", ""), - ai_agents=[], # Initialize empty list for agents + ai_agents=[], is_alive=True, ).save() logger.info(f"Project '{project.name}' created successfully") @@ -67,11 +46,26 @@ def post(self): logger.error(f"Error creating project: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self, id): - """PUT /projects/ -> Update a project.""" + +class ProjectDetailAPI(MethodView): + decorators = [login_required] + + def get(self): + try: + project_id = request.args.get("project_id") + project = Project.objects.get(id=ObjectId(project_id)) + serialized = ProjectSerializer().dump(project) + logger.info(f"Fetched project: {project_id}") + return jsonify({"message": f"Successfully fetched project: {project_id}", "data": serialized}) + except Exception as e: + logger.error(f"Error fetching project: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self): try: data = request.json - project = Project.objects.get(id=ObjectId(id)) + project_id = data.get("project_id") + project = Project.objects.get(id=ObjectId(project_id)) project.name = data.get("name", project.name) project.description = data.get("description", project.description) project.save() @@ -81,13 +75,19 @@ def put(self, id): logger.error(f"Error updating project: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self, id): - """DELETE /projects/ -> Delete a project.""" + def delete(self): try: - project = Project.objects.get(id=ObjectId(id)) + project_id = request.args.get("project_id") + project = Project.objects.get(id=ObjectId(project_id)) project.delete() logger.info("Project deleted successfully") return jsonify({"message": "Project deleted successfully"}), 200 except Exception as e: logger.error(f"Error deleting project: {str(e)}") return jsonify({"error": str(e)}), 500 + + +project_bp.add_url_rule("/", view_func=ProjectListAPI.as_view("project_list_api"), methods=["GET", "POST"]) +project_bp.add_url_rule( + "/detail", view_func=ProjectDetailAPI.as_view("project_detail_api"), methods=["GET", "PUT", "DELETE"] +) From 8a660c95a8772774911f38c87f409a156190ff0f Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 12:52:39 +0530 Subject: [PATCH 39/62] fix --- server/app/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index 5dc303a..e810271 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -5,7 +5,7 @@ from flask import Blueprint, Flask from flask_cors import CORS -from .api.agent_variant_view import variant_bp +from .api.agent_variant_view import agent_variant_bp from .api.ai_agent_view import ai_agent_bp from .api.auth_view import auth_bp from .api.config_view import config_bp @@ -28,7 +28,7 @@ def register_api_blueprints(app): api_bp.register_blueprint(evaluation_bp) api_bp.register_blueprint(dataset_bp) api_bp.register_blueprint(ai_agent_bp) - api_bp.register_blueprint(variant_bp) + api_bp.register_blueprint(agent_variant_bp) app.register_blueprint(api_bp) From e641f829fa33e89612e29e95e2a113531081a720 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 13:02:13 +0530 Subject: [PATCH 40/62] projects api tested and finalized --- server/app/api/project_view.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/server/app/api/project_view.py b/server/app/api/project_view.py index bda632a..bca21dd 100644 --- a/server/app/api/project_view.py +++ b/server/app/api/project_view.py @@ -50,9 +50,8 @@ def post(self): class ProjectDetailAPI(MethodView): decorators = [login_required] - def get(self): + def get(self, project_id): try: - project_id = request.args.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) serialized = ProjectSerializer().dump(project) logger.info(f"Fetched project: {project_id}") @@ -61,10 +60,9 @@ def get(self): logger.error(f"Error fetching project: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self): + def put(self, project_id): try: data = request.json - project_id = data.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) project.name = data.get("name", project.name) project.description = data.get("description", project.description) @@ -75,9 +73,8 @@ def put(self): logger.error(f"Error updating project: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self): + def delete(self, project_id): try: - project_id = request.args.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) project.delete() logger.info("Project deleted successfully") @@ -89,5 +86,5 @@ def delete(self): project_bp.add_url_rule("/", view_func=ProjectListAPI.as_view("project_list_api"), methods=["GET", "POST"]) project_bp.add_url_rule( - "/detail", view_func=ProjectDetailAPI.as_view("project_detail_api"), methods=["GET", "PUT", "DELETE"] + "/", view_func=ProjectDetailAPI.as_view("project_detail_api"), methods=["GET", "PUT", "DELETE"] ) From 977cb5aea63ac424b6e63c20b5998db0adcb8c9a Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 13:03:48 +0530 Subject: [PATCH 41/62] minor rename --- server/app/__init__.py | 2 +- .../app/api/{agent_variant_view.py => ai_agent_variant_view.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename server/app/api/{agent_variant_view.py => ai_agent_variant_view.py} (100%) diff --git a/server/app/__init__.py b/server/app/__init__.py index e810271..4c52356 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -5,7 +5,7 @@ from flask import Blueprint, Flask from flask_cors import CORS -from .api.agent_variant_view import agent_variant_bp +from .api.ai_agent_variant_view import agent_variant_bp from .api.ai_agent_view import ai_agent_bp from .api.auth_view import auth_bp from .api.config_view import config_bp diff --git a/server/app/api/agent_variant_view.py b/server/app/api/ai_agent_variant_view.py similarity index 100% rename from server/app/api/agent_variant_view.py rename to server/app/api/ai_agent_variant_view.py From 41bd9bff1df3ea65be6319531103bc9cc3904b56 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 13:52:55 +0530 Subject: [PATCH 42/62] tested ai agents views --- server/app/api/ai_agent_view.py | 45 +++++++++++++++++++-------------- server/app/models.py | 3 +++ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/server/app/api/ai_agent_view.py b/server/app/api/ai_agent_view.py index 0b10899..fcb11bf 100644 --- a/server/app/api/ai_agent_view.py +++ b/server/app/api/ai_agent_view.py @@ -9,15 +9,16 @@ from ..serializers import AIAgentSerializer logger = Logging.get_logger(__name__) -ai_agent_bp = Blueprint("ai_agent", __name__, url_prefix="/agents") + +ai_agent_bp = Blueprint("ai_agent", __name__, url_prefix="/projects") class AIAgentAPI(MethodView): decorators = [login_required] - def get(self): + def get(self, project_id): + """Fetch all AI agents for a given project.""" try: - project_id = request.args.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) agents = AIAgentSerializer(many=True).dump(project.ai_agents) logger.info(f"Successfully fetched AI agents for project: {project_id}") @@ -29,10 +30,10 @@ def get(self): logger.error(f"Error fetching AI agents: {str(e)}") return jsonify({"error": str(e)}), 500 - def post(self): + def post(self, project_id): + """Create a new AI agent for a given project.""" try: data = request.json - project_id = data.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) new_agent = AIAgent( name=data["name"], @@ -53,12 +54,11 @@ def post(self): class AIAgentDetailAPI(MethodView): decorators = [login_required] - def get(self): + def get(self, project_id, agent_id): + """Retrieve details for a specific AI agent in a project.""" try: - project_id = request.args.get("project_id") - agent_id = request.args.get("agent_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if agent: return jsonify(AIAgentSerializer().dump(agent)), 200 return jsonify({"error": "Agent not found"}), 404 @@ -66,15 +66,15 @@ def get(self): logger.error(f"Error retrieving AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 - def put(self): + def put(self, project_id, agent_id): + """Update a specific AI agent in a project.""" try: data = request.json - project_id = data.get("project_id") - agent_id = data.get("agent_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 + agent.name = data.get("name", agent.name) agent.agent_config = data.get("config", agent.agent_config) agent.prompt = data.get("prompt", agent.prompt) @@ -86,14 +86,14 @@ def put(self): logger.error(f"Error updating AI agent: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self): + def delete(self, project_id, agent_id): + """Delete a specific AI agent from a project.""" try: - project_id = request.args.get("project_id") - agent_id = request.args.get("agent_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 + project.ai_agents.remove(agent) project.save() logger.info("AI Agent deleted successfully") @@ -107,6 +107,7 @@ class ConversationAPI(MethodView): decorators = [login_required] def post(self): + """Generate a conversation (with optional streaming) from the provided prompt and configuration.""" try: data = request.json config = data.get("config") @@ -132,8 +133,14 @@ def post(self): return jsonify({"error": str(e)}), 500 -ai_agent_bp.add_url_rule("/", view_func=AIAgentAPI.as_view("ai_agent_api"), methods=["GET", "POST"]) +# Agents endpoints are nested under /projects//agents ai_agent_bp.add_url_rule( - "/detail", view_func=AIAgentDetailAPI.as_view("ai_agent_detail_api"), methods=["GET", "PUT", "DELETE"] + "//agents/", view_func=AIAgentAPI.as_view("ai_agent_api"), methods=["GET", "POST"] ) +ai_agent_bp.add_url_rule( + "//agents/", + view_func=AIAgentDetailAPI.as_view("ai_agent_detail_api"), + methods=["GET", "PUT", "DELETE"], +) + ai_agent_bp.add_url_rule("/conversation", view_func=ConversationAPI.as_view("conversation_api"), methods=["POST"]) diff --git a/server/app/models.py b/server/app/models.py index 6958cee..eb0076e 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -1,4 +1,5 @@ # models.py +from bson import ObjectId from mongoengine import ( DENY, BooleanField, @@ -9,6 +10,7 @@ EmbeddedDocument, EmbeddedDocumentField, ListField, + ObjectIdField, ReferenceField, StringField, ) @@ -33,6 +35,7 @@ def save(self, *args, **kwargs): class BaseEmbeddedDocument(EmbeddedDocument): + id = ObjectIdField(default=lambda: ObjectId()) is_alive = BooleanField(default=True) added_on = DateTimeField(default=get_datetime_in_ist()) updated_on = DateTimeField(default=get_datetime_in_ist()) From 84dca9ef15fa84cb963b2f8a6260fcea642058f2 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:05:25 +0530 Subject: [PATCH 43/62] tested ai agent variant routes --- server/app/api/ai_agent_variant_view.py | 52 ++++++++++++++----------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/server/app/api/ai_agent_variant_view.py b/server/app/api/ai_agent_variant_view.py index 39b0822..1cf3ec4 100644 --- a/server/app/api/ai_agent_variant_view.py +++ b/server/app/api/ai_agent_variant_view.py @@ -8,18 +8,16 @@ from ..serializers import AIAgentVariantSerializer logger = Logging.get_logger(__name__) -agent_variant_bp = Blueprint("agent_variant", __name__, url_prefix="/variants") +agent_variant_bp = Blueprint("agent_variant", __name__, url_prefix="/projects//agents//variants") class VariantListAPI(MethodView): decorators = [login_required] - def get(self): + def get(self, project_id, agent_id): try: - project_id = request.args.get("project_id") - agent_id = request.args.get("agent_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 serialized = AIAgentVariantSerializer(many=True).dump(agent.variants) @@ -29,15 +27,13 @@ def get(self): logger.error(f"Error fetching variants: {str(e)}") return jsonify({"error": str(e)}), 500 - def post(self): + def post(self, project_id, agent_id): try: data = request.json - project_id = data.get("project_id") - agent_id = data.get("agent_id") variant_name = data.get("name") variables = data.get("variables") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 if not set(variables.keys()).issubset(set(agent.included_variables)): @@ -55,17 +51,30 @@ def post(self): class VariantDetailAPI(MethodView): decorators = [login_required] - def put(self): + def get(self, project_id, agent_id, variant_id): + try: + project = Project.objects.get(id=ObjectId(project_id)) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 + variant = next((v for v in agent.variants if v.id == ObjectId(variant_id)), None) + if not variant: + return jsonify({"error": "Variant not found"}), 404 + serialized = AIAgentVariantSerializer().dump(variant) + logger.info("Successfully fetched variant") + return jsonify({"message": "Variant fetched successfully", "data": serialized}), 200 + except Exception as e: + logger.error(f"Error fetching variant: {str(e)}") + return jsonify({"error": str(e)}), 500 + + def put(self, project_id, agent_id, variant_id): try: data = request.json - project_id = data.get("project_id") - agent_id = data.get("agent_id") - variant_id = data.get("variant_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + variant = next((v for v in agent.variants if v.id == ObjectId(variant_id)), None) if not variant: return jsonify({"error": "Variant not found"}), 404 variant.name = data.get("name", variant.name) @@ -77,16 +86,13 @@ def put(self): logger.error(f"Error updating variant: {str(e)}") return jsonify({"error": str(e)}), 500 - def delete(self): + def delete(self, project_id, agent_id, variant_id): try: - project_id = request.args.get("project_id") - agent_id = request.args.get("agent_id") - variant_id = request.args.get("variant_id") project = Project.objects.get(id=ObjectId(project_id)) - agent = next((a for a in project.ai_agents if a.agent_id == agent_id), None) + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) if not agent: return jsonify({"error": "Agent not found"}), 404 - variant = next((v for v in agent.variants if v.variant_id == variant_id), None) + variant = next((v for v in agent.variants if v.id == ObjectId(variant_id)), None) if not variant: return jsonify({"error": "Variant not found"}), 404 agent.variants.remove(variant) @@ -99,4 +105,6 @@ def delete(self): agent_variant_bp.add_url_rule("/", view_func=VariantListAPI.as_view("variant_list_api"), methods=["GET", "POST"]) -agent_variant_bp.add_url_rule("/", view_func=VariantDetailAPI.as_view("variant_detail_api"), methods=["PUT", "DELETE"]) +agent_variant_bp.add_url_rule( + "/", view_func=VariantDetailAPI.as_view("variant_detail_api"), methods=["GET", "PUT", "DELETE"] +) From 86c3f7e7e4d9e1ed95b8aea704082cb798f23f31 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:08:45 +0530 Subject: [PATCH 44/62] modified dataset view to be more restful --- server/app/api/dataset_view.py | 108 ++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 49 deletions(-) diff --git a/server/app/api/dataset_view.py b/server/app/api/dataset_view.py index 27d9464..36913a9 100644 --- a/server/app/api/dataset_view.py +++ b/server/app/api/dataset_view.py @@ -8,28 +8,26 @@ from ..serializers import DatasetSerializer logger = Logging.get_logger(__name__) -dataset_bp = Blueprint("dataset", __name__, url_prefix="//datasets") +dataset_bp = Blueprint("dataset", __name__, url_prefix="/projects//datasets") class DatasetListAPI(MethodView): decorators = [login_required] - def get(self): + def get(self, project_id): try: - project_id = request.args.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) datasets = Dataset.objects.filter(project=project, is_alive=True) serialized = DatasetSerializer(many=True).dump(datasets) logger.info(f"Fetched datasets for project: {project_id}") - return jsonify({"message": "Successfully fetched datasets", "data": serialized}) + return jsonify({"message": "Successfully fetched datasets", "data": serialized}), 200 except Exception as e: logger.error(f"Error fetching datasets: {str(e)}") return jsonify({"message": str(e)}), 500 - def post(self): + def post(self, project_id): try: data = request.json - project_id = data.get("project_id") project = Project.objects.get(id=ObjectId(project_id)) dataset = Dataset( project=project, @@ -48,31 +46,42 @@ def post(self): class DatasetDetailAPI(MethodView): decorators = [login_required] - def put(self): + def get(self, project_id, dataset_id): + try: + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) + serialized = DatasetSerializer().dump(dataset) + logger.info(f"Fetched dataset: {dataset_id}") + return jsonify({"message": "Dataset fetched successfully", "data": serialized}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 + except Exception as e: + logger.error(f"Error fetching dataset: {str(e)}") + return jsonify({"message": str(e)}), 500 + + def put(self, project_id, dataset_id): try: data = request.json - dataset_id = data.get("dataset_id") - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) dataset.name = data.get("name", dataset.name) + dataset.type = data.get("type", dataset.type) dataset.samples = data.get("samples", dataset.samples) dataset.save() logger.info(f"Dataset '{dataset.name}' updated successfully") return jsonify({"message": f"Dataset '{dataset.name}' updated successfully"}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 except Exception as e: logger.error(f"Error updating dataset: {str(e)}") return jsonify({"message": str(e)}), 500 - def delete(self): + def delete(self, project_id, dataset_id): try: - dataset_id = request.args.get("dataset_id") - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) dataset.delete() logger.info(f"Dataset deleted successfully with id: {dataset_id}") - return jsonify({"message": "Dataset deleted successfully with id: " + dataset_id}), 200 + return jsonify({"message": f"Dataset deleted successfully with id: {dataset_id}"}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 except Exception as e: logger.error(f"Error deleting dataset: {str(e)}") return jsonify({"message": str(e)}), 500 @@ -81,13 +90,10 @@ def delete(self): class DatasetSampleAPI(MethodView): decorators = [login_required] - def post(self): + def post(self, project_id, dataset_id): try: data = request.json - dataset_id = data.get("dataset_id") - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) sample = data.get("sample") or {} if sample in dataset.samples: return jsonify({"message": "Sample already exists"}), 400 @@ -95,53 +101,57 @@ def post(self): dataset.save() logger.info(f"Sample added to dataset '{dataset_id}'") return jsonify({"message": f"Sample added to dataset '{dataset_id}' successfully"}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 except Exception as e: logger.error(f"Error adding sample: {str(e)}") return jsonify({"message": str(e)}), 500 - def delete(self): + def delete(self, project_id, dataset_id, sample_id): try: - data = request.json - dataset_id = data.get("dataset_id") - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - sample = data.get("sample") or {} - if sample in dataset.samples: - dataset.samples.remove(sample) - dataset.save() - logger.info(f"Sample removed from dataset '{dataset_id}'") - return jsonify({"message": f"Sample removed from dataset '{dataset_id}' successfully"}), 200 - else: + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) + sample = next((s for s in dataset.samples if s.get("id") == sample_id), None) + if not sample: return jsonify({"message": "Sample not found"}), 404 + dataset.samples.remove(sample) + dataset.save() + logger.info(f"Sample removed from dataset '{dataset_id}'") + return jsonify({"message": f"Sample removed successfully"}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 except Exception as e: logger.error(f"Error removing sample: {str(e)}") return jsonify({"message": str(e)}), 500 - def put(self): + def put(self, project_id, dataset_id, sample_id): try: data = request.json - dataset_id = data.get("dataset_id") - dataset = Dataset.objects.get(id=dataset_id) - if not dataset: - return jsonify({"message": "Dataset not found"}), 404 - sample = data.get("sample") or {} - updated_sample = data.get("updated_sample") or {} - try: - index = dataset.samples.index(sample) - except ValueError: + dataset = Dataset.objects.get(id=ObjectId(dataset_id), project__id=ObjectId(project_id)) + sample = next((s for s in dataset.samples if s.get("id") == sample_id), None) + if not sample: return jsonify({"message": "Sample not found"}), 404 + updated_sample = data.get("updated_sample") or {} + index = dataset.samples.index(sample) dataset.samples[index] = updated_sample dataset.save() logger.info(f"Sample updated in dataset '{dataset_id}'") - return jsonify({"message": f"Sample updated in dataset '{dataset_id}' successfully"}), 200 + return jsonify({"message": "Sample updated successfully"}), 200 + except Dataset.DoesNotExist: + return jsonify({"message": "Dataset not found"}), 404 except Exception as e: logger.error(f"Error updating sample: {str(e)}") return jsonify({"message": str(e)}), 500 -dataset_bp.add_url_rule("/", view_func=DatasetListAPI.as_view("dataset_list_api"), methods=["GET", "POST"]) -dataset_bp.add_url_rule("/", view_func=DatasetDetailAPI.as_view("dataset_detail_api"), methods=["PUT", "DELETE"]) +dataset_bp.add_url_rule("", view_func=DatasetListAPI.as_view("dataset_list_api"), methods=["GET", "POST"]) +dataset_bp.add_url_rule( + "/", view_func=DatasetDetailAPI.as_view("dataset_detail_api"), methods=["GET", "PUT", "DELETE"] +) +dataset_bp.add_url_rule( + "//samples", view_func=DatasetSampleAPI.as_view("dataset_sample_api"), methods=["POST"] +) dataset_bp.add_url_rule( - "/samples", view_func=DatasetSampleAPI.as_view("dataset_sample_api"), methods=["POST", "DELETE", "PUT"] + "//samples/", + view_func=DatasetSampleAPI.as_view("dataset_sample_detail_api"), + methods=["PUT", "DELETE"], ) From 5a45e528f097b49702a63d0d459be8c9e16628a0 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:09:52 +0530 Subject: [PATCH 45/62] created api_views folder instead of api --- server/app/__init__.py | 16 ++++++++-------- server/app/{api => api_views}/__init__.py | 0 .../{api => api_views}/ai_agent_variant_view.py | 0 server/app/{api => api_views}/ai_agent_view.py | 0 server/app/{api => api_views}/auth_view.py | 0 server/app/{api => api_views}/config_view.py | 0 server/app/{api => api_views}/dataset_view.py | 0 server/app/{api => api_views}/evaluation_view.py | 0 server/app/{api => api_views}/main_view.py | 0 server/app/{api => api_views}/project_view.py | 0 10 files changed, 8 insertions(+), 8 deletions(-) rename server/app/{api => api_views}/__init__.py (100%) rename server/app/{api => api_views}/ai_agent_variant_view.py (100%) rename server/app/{api => api_views}/ai_agent_view.py (100%) rename server/app/{api => api_views}/auth_view.py (100%) rename server/app/{api => api_views}/config_view.py (100%) rename server/app/{api => api_views}/dataset_view.py (100%) rename server/app/{api => api_views}/evaluation_view.py (100%) rename server/app/{api => api_views}/main_view.py (100%) rename server/app/{api => api_views}/project_view.py (100%) diff --git a/server/app/__init__.py b/server/app/__init__.py index 4c52356..9fe3e58 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -5,14 +5,14 @@ from flask import Blueprint, Flask from flask_cors import CORS -from .api.ai_agent_variant_view import agent_variant_bp -from .api.ai_agent_view import ai_agent_bp -from .api.auth_view import auth_bp -from .api.config_view import config_bp -from .api.dataset_view import dataset_bp -from .api.evaluation_view import evaluation_bp -from .api.main_view import main_bp -from .api.project_view import project_bp +from .api_views.ai_agent_variant_view import agent_variant_bp +from .api_views.ai_agent_view import ai_agent_bp +from .api_views.auth_view import auth_bp +from .api_views.config_view import config_bp +from .api_views.dataset_view import dataset_bp +from .api_views.evaluation_view import evaluation_bp +from .api_views.main_view import main_bp +from .api_views.project_view import project_bp from .config import Config, Logging # Initialize logging once diff --git a/server/app/api/__init__.py b/server/app/api_views/__init__.py similarity index 100% rename from server/app/api/__init__.py rename to server/app/api_views/__init__.py diff --git a/server/app/api/ai_agent_variant_view.py b/server/app/api_views/ai_agent_variant_view.py similarity index 100% rename from server/app/api/ai_agent_variant_view.py rename to server/app/api_views/ai_agent_variant_view.py diff --git a/server/app/api/ai_agent_view.py b/server/app/api_views/ai_agent_view.py similarity index 100% rename from server/app/api/ai_agent_view.py rename to server/app/api_views/ai_agent_view.py diff --git a/server/app/api/auth_view.py b/server/app/api_views/auth_view.py similarity index 100% rename from server/app/api/auth_view.py rename to server/app/api_views/auth_view.py diff --git a/server/app/api/config_view.py b/server/app/api_views/config_view.py similarity index 100% rename from server/app/api/config_view.py rename to server/app/api_views/config_view.py diff --git a/server/app/api/dataset_view.py b/server/app/api_views/dataset_view.py similarity index 100% rename from server/app/api/dataset_view.py rename to server/app/api_views/dataset_view.py diff --git a/server/app/api/evaluation_view.py b/server/app/api_views/evaluation_view.py similarity index 100% rename from server/app/api/evaluation_view.py rename to server/app/api_views/evaluation_view.py diff --git a/server/app/api/main_view.py b/server/app/api_views/main_view.py similarity index 100% rename from server/app/api/main_view.py rename to server/app/api_views/main_view.py diff --git a/server/app/api/project_view.py b/server/app/api_views/project_view.py similarity index 100% rename from server/app/api/project_view.py rename to server/app/api_views/project_view.py From b09ed3dad31fc7df3b17ec14f0986b6ee2ddc703 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:12:13 +0530 Subject: [PATCH 46/62] modified evaluation_view to be more restful --- server/app/api_views/evaluation_view.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/server/app/api_views/evaluation_view.py b/server/app/api_views/evaluation_view.py index 5f80aad..e0f06d5 100644 --- a/server/app/api_views/evaluation_view.py +++ b/server/app/api_views/evaluation_view.py @@ -16,21 +16,18 @@ class EvaluationAPI(MethodView): - def post(self): + def post(self, project_id, agent_id): try: data = request.json - project_id = data.get("project_id") - agent_id = data.get("agent_id") input_text = data.get("input_text") output_text = data.get("output_text") - if not all([project_id, agent_id, input_text, output_text]): - return jsonify({"error": "project_id, agent_id, input_text and output_text are required"}), 400 + if not all([input_text, output_text]): + return jsonify({"error": "input_text and output_text are required"}), 400 project = Project.objects.get(id=ObjectId(project_id)) - try: - agent = project.ai_agents[int(agent_id)] - except (IndexError, ValueError): - return jsonify({"error": "Invalid agent reference"}), 400 + agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) + if not agent: + return jsonify({"error": "Agent not found"}), 404 evaluator_config = agent.agent_config or {} strategy_type = evaluator_config.get("strategy", "llm").lower() @@ -56,4 +53,6 @@ def post(self): return jsonify({"error": str(e)}), 500 -evaluation_bp.add_url_rule("/", view_func=EvaluationAPI.as_view("evaluation_api"), methods=["POST"]) +evaluation_bp.add_url_rule( + "//", view_func=EvaluationAPI.as_view("evaluation_api"), methods=["POST"] +) From 93627d0efae8542bb4dfbb87c8669e289d19dac5 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:48:46 +0530 Subject: [PATCH 47/62] llm evaluation tested --- server/app/api_views/evaluation_view.py | 23 +++-- .../app/core/services/evaluation_services.py | 86 +++++++++++-------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/server/app/api_views/evaluation_view.py b/server/app/api_views/evaluation_view.py index e0f06d5..974474a 100644 --- a/server/app/api_views/evaluation_view.py +++ b/server/app/api_views/evaluation_view.py @@ -19,10 +19,11 @@ class EvaluationAPI(MethodView): def post(self, project_id, agent_id): try: data = request.json - input_text = data.get("input_text") - output_text = data.get("output_text") - if not all([input_text, output_text]): - return jsonify({"error": "input_text and output_text are required"}), 400 + input = data.get("input") + output = data.get("output") + instructions = data.get("instructions") + if not all([input, output]): + return jsonify({"error": "input and output are required"}), 400 project = Project.objects.get(id=ObjectId(project_id)) agent = next((a for a in project.ai_agents if a.id == ObjectId(agent_id)), None) @@ -31,9 +32,13 @@ def post(self, project_id, agent_id): evaluator_config = agent.agent_config or {} strategy_type = evaluator_config.get("strategy", "llm").lower() + if strategy_type == "llm": - instructions = evaluator_config.get("instructions") - evaluator_strategy = LLMEvaluationStrategy(config=evaluator_config, instructions=instructions) + base_guidelines = evaluator_config.get("base_guidelines") + if not base_guidelines and instructions: + base_guidelines = instructions + + evaluator_strategy = LLMEvaluationStrategy(config=evaluator_config, guidelines=base_guidelines) elif strategy_type == "embedding": embedding_model = evaluator_config.get("embedding_model") if not embedding_model: @@ -44,9 +49,9 @@ def post(self, project_id, agent_id): else: return jsonify({"error": "Unknown evaluation strategy"}), 400 - instructions = agent.prompt[0]["content"] if agent.prompt else "" - evaluator = Evaluator(instructions=instructions, config=evaluator_config, strategy=evaluator_strategy) - score = evaluator.evaluate_pair(input_text, output_text) + evaluator = Evaluator(config=evaluator_config, strategy=evaluator_strategy) + score = evaluator.evaluate_pair(input, output) + return jsonify({"score": score}), 200 except Exception as e: logger.error(f"Error during evaluation: {e}") diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index d601aa7..8eabb34 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod import numpy as np -from flask import Config from fuzzywuzzy import fuzz from server.app.core.services.llm_response_service import LLMResponseService -from ...config import Logging +from ...config import Config, Logging logger = Logging.get_logger(__name__) @@ -26,35 +25,56 @@ class LLMEvaluationStrategy(EvaluationStrategy): Strategy using an LLM for evaluation """ - def __init__(self, config: dict, instructions: str): + def __init__(self, config: dict, guidelines: str): self.config = config self.model = config.get("model", "gpt-4o") self.max_tokens = config.get("max_tokens", Config.DEFAULT_MAX_TOKENS) self.temperature = config.get("temperature", Config.DEFAULT_TEMPERATURE) - self.instructions = instructions - - def __get_prompt(self, input_text: str, output_text: str): - prompt = "" - if self.instructions: - prompt = f"Evaluate the following output given the input and instructions:\n" - prompt += f"Instructions: {self.instructions}\n" - prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" + self.guidelines = guidelines + + def _get_prompt(self, input_text: str, output_text: str): + if self.guidelines: + prompt = [ + { + "role": "system", + "content": "You are an evaluator. Your task is to evaluate the output based on the given input and instructions.", + }, + {"role": "user", "content": f"Instructions: {self.guidelines}"}, + {"role": "user", "content": f"Input: {input_text}"}, + {"role": "user", "content": f"Output: {output_text}"}, + {"role": "user", "content": "Score the output between either 0 or 1."}, + ] else: - prompt = f"Note: The score should be between 0 and 100. The higher the score, the better the output." - prompt += f"Evaluate the following output given the input:\n" - prompt += f"Input: {input_text}\nOutput: {output_text}\nScore:" + prompt = [ + { + "role": "system", + "content": "You are an evaluator. Your task is to evaluate the output based on the given input.", + }, + {"role": "user", "content": f"Input: {input_text}"}, + {"role": "user", "content": f"Output: {output_text}"}, + {"role": "user", "content": "Score the output between either 0 or 1."}, + ] return prompt - def __validate_response_conformity_with_instructions(self, response: str): - if self.instructions: - prompt = f"These are the instructions: {self.instructions}\n" - prompt += f"This is the Evaluator's response: {response}\n" - prompt += f"Check if the response is consistent with the instructions. If it is, return 'True'. If it is not, return 'False'." + def _validate_response_conformity_with_instructions(self, response: str): + if self.guidelines: + prompt = [ + { + "role": "system", + "content": "You are an evaluator. Your task is to evaluate the output based on the given input and instructions.", + }, + {"role": "user", "content": f"Instructions: {self.guidelines}"}, + {"role": "user", "content": f"Evaluator's response: {response}"}, + { + "role": "user", + "content": "Check if the response is consistent with the instructions. If it is, return 'True'. If it is not, return 'False'.", + }, + ] # initialize LLMResponseService llm_response_service = LLMResponseService( model=self.model, - prompt=[{"role": "user", "content": prompt}], + prompt=prompt, max_tokens=self.max_tokens, temperature=self.temperature, ) @@ -71,11 +91,11 @@ def __validate_response_conformity_with_instructions(self, response: str): return False else: # If there are no instructions, check if score is between 0 and 100 - if 0 <= int(response) <= 100: - logger.info(f"Response is between 0 and 100. Response: {response}") + if 0 <= int(response) <= 1: + logger.info(f"Response is between 0 and 1. Response: {response}") return True else: - logger.error(f"Response is not between 0 and 100. Response: {response}") + logger.error(f"Response is not between 0 and 1. Response: {response}") return False def evaluate(self, input_text: str, output_text: str, max_retries: int = 3): @@ -84,7 +104,7 @@ def evaluate(self, input_text: str, output_text: str, max_retries: int = 3): # unpack config and initialize LLMResponseService llm_response_service = LLMResponseService( model=self.model, - prompt=[{"role": "user", "content": prompt}], + prompt=prompt, max_tokens=self.max_tokens, temperature=self.temperature, ) @@ -139,20 +159,14 @@ def evaluate(self, input_text: str, output_text: str, **kwargs): class Evaluator: """ - Evaluator class takes instructions, config and a strategy instance + Evaluator class takes config and a strategy instance """ - def __init__(self, instructions: str, config: dict, strategy: EvaluationStrategy): - self.instructions = instructions + def __init__(self, config: dict, strategy: EvaluationStrategy): self.config = config self.strategy = strategy - def evaluate_pair(self, input_text: str, output_text: str): - score = self.strategy.evaluate(input_text, output_text, instructions=self.instructions) - return { - "instructions": self.instructions, - "input": input_text, - "output": output_text, - "score": score, - "config": self.config, - } + def evaluate_pair(self, input: str, output: str): + score = self.strategy.evaluate(input, output) + logger.info(f"Evaluator scored {score} for input: {input} and output: {output}") + return score From 8138cf9e6feb3a3a4dcb4846256a0ea1e4001125 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:51:20 +0530 Subject: [PATCH 48/62] minor rename --- server/app/api_views/ai_agent_view.py | 2 +- server/app/core/services/evaluation_services.py | 2 +- .../core/services/{llm_response_service.py => llm_service.py} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename server/app/core/services/{llm_response_service.py => llm_service.py} (100%) diff --git a/server/app/api_views/ai_agent_view.py b/server/app/api_views/ai_agent_view.py index fcb11bf..3713cb2 100644 --- a/server/app/api_views/ai_agent_view.py +++ b/server/app/api_views/ai_agent_view.py @@ -4,7 +4,7 @@ from ..auth.decorators import login_required from ..config import Config, Logging -from ..core.services.llm_response_service import LLMResponseService +from ..core.services.llm_service import LLMResponseService from ..models import AIAgent, Project from ..serializers import AIAgentSerializer diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index 8eabb34..ae2b5cc 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -3,7 +3,7 @@ import numpy as np from fuzzywuzzy import fuzz -from server.app.core.services.llm_response_service import LLMResponseService +from server.app.core.services.llm_service import LLMResponseService from ...config import Config, Logging diff --git a/server/app/core/services/llm_response_service.py b/server/app/core/services/llm_service.py similarity index 100% rename from server/app/core/services/llm_response_service.py rename to server/app/core/services/llm_service.py From 4c299109ba610919750c304647061d17d0d04f34 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:51:44 +0530 Subject: [PATCH 49/62] minor changes --- server/app/core/services/evaluation_services.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index ae2b5cc..1aa9d64 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -3,9 +3,8 @@ import numpy as np from fuzzywuzzy import fuzz -from server.app.core.services.llm_service import LLMResponseService - from ...config import Config, Logging +from ..services.llm_service import LLMResponseService logger = Logging.get_logger(__name__) From 9e32e5273f98009a5e2e7d71b03b51382d4dbbfd Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 14:54:20 +0530 Subject: [PATCH 50/62] added embedding similarity metric --- server/app/api_views/evaluation_view.py | 5 +++- .../app/core/services/evaluation_services.py | 29 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/server/app/api_views/evaluation_view.py b/server/app/api_views/evaluation_view.py index 974474a..ea343e5 100644 --- a/server/app/api_views/evaluation_view.py +++ b/server/app/api_views/evaluation_view.py @@ -41,9 +41,12 @@ def post(self, project_id, agent_id): evaluator_strategy = LLMEvaluationStrategy(config=evaluator_config, guidelines=base_guidelines) elif strategy_type == "embedding": embedding_model = evaluator_config.get("embedding_model") + similarity_metric = evaluator_config.get("similarity_metric", "cosine") if not embedding_model: return jsonify({"error": "Embedding model not provided in evaluator config"}), 400 - evaluator_strategy = EmbeddingEvaluationStrategy(embedding_model=embedding_model) + evaluator_strategy = EmbeddingEvaluationStrategy( + embedding_model=embedding_model, similarity_metric=similarity_metric + ) elif strategy_type == "fuzzy": evaluator_strategy = FuzzyEvaluationStrategy() else: diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index 1aa9d64..f08fc55 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -130,20 +130,31 @@ def evaluate(self, input_text: str, output_text: str, max_retries: int = 3): class EmbeddingEvaluationStrategy(EvaluationStrategy): """ - Strategy using embedding similarity for evaluation + Strategy using embedding similarity for evaluation. + Supports multiple similarity metrics. """ - def __init__(self, embedding_model): + def __init__(self, embedding_model, similarity_metric: str = "cosine"): self.embedding_model = embedding_model + self.similarity_metric = similarity_metric def evaluate(self, input_text: str, output_text: str, **kwargs): - # Compute embeddings for input and output - input_emb = self.embedding_model.encode(input_text) - output_emb = self.embedding_model.encode(output_text) - cosine_similarity = np.dot(input_emb, output_emb) / ( - np.linalg.norm(input_emb) * np.linalg.norm(output_emb) + 1e-8 - ) - return cosine_similarity + try: + input_emb = self.embedding_model.encode(input_text) + output_emb = self.embedding_model.encode(output_text) + + if self.similarity_metric == "cosine": + similarity = np.dot(input_emb, output_emb) / ( + np.linalg.norm(input_emb) * np.linalg.norm(output_emb) + 1e-8 + ) + elif self.similarity_metric == "euclidean": + similarity = np.linalg.norm(input_emb - output_emb) + else: + raise ValueError(f"Unsupported similarity metric: {self.similarity_metric}") + return similarity + except Exception as e: + logger.error(f"Error in embedding evaluation: {e}") + return None class FuzzyEvaluationStrategy(EvaluationStrategy): From 6c34ca91919cb3c9db86cdb463f61eeaebe0c7c4 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:28:29 +0530 Subject: [PATCH 51/62] added embedding service --- server/app/core/services/embedding_service.py | 89 +++++++++++++ server/app/core/services/llm_service.py | 26 ++-- server/app/utils/api_clients.py | 117 ++++++++++-------- 3 files changed, 164 insertions(+), 68 deletions(-) create mode 100644 server/app/core/services/embedding_service.py diff --git a/server/app/core/services/embedding_service.py b/server/app/core/services/embedding_service.py new file mode 100644 index 0000000..fef4365 --- /dev/null +++ b/server/app/core/services/embedding_service.py @@ -0,0 +1,89 @@ +from abc import ABC, abstractmethod + +import numpy as np +from anthropic import Anthropic +from openai import OpenAI + +from ...config import Config, Logging + +logger = Logging.get_logger(__name__) + + +class EmbeddingModel(ABC): + """ + Abstract base class for embedding models. + """ + + @abstractmethod + def encode(self, text: str) -> np.ndarray: + pass + + +class OpenAIEmbeddingModel(EmbeddingModel): + def __init__(self, model_name: str): + self.model_name = model_name + self.client = OpenAI(api_key=Config.OPENAI_API_KEY) + + def encode(self, text: str) -> np.ndarray: + response = self.client.embeddings.create(input=text, model=self.model_name) + return np.array(response.data[0].embedding) + + +class ClaudeEmbeddingModel(EmbeddingModel): + def __init__(self, model_name: str): + self.model_name = model_name + self.client = Anthropic(api_key=Config.CLAUDE_API_KEY) + + def encode(self, text: str) -> np.ndarray: + response = self.client.embeddings.create(input=text, model=self.model_name) + return np.array(response.data[0].embedding) + + +class SimilarityMetric(ABC): + """ + Abstract base class for similarity metrics. + """ + + @abstractmethod + def compute(self, vector_a: np.ndarray, vector_b: np.ndarray) -> float: + pass + + +class CosineSimilarity(SimilarityMetric): + def compute(self, vector_a: np.ndarray, vector_b: np.ndarray) -> float: + return np.dot(vector_a, vector_b) / (np.linalg.norm(vector_a) * np.linalg.norm(vector_b) + 1e-8) + + +class EuclideanDistance(SimilarityMetric): + def compute(self, vector_a: np.ndarray, vector_b: np.ndarray) -> float: + return np.linalg.norm(vector_a - vector_b) + + +class EmbeddingService: + """ + Service to handle embedding operations. + """ + + def __init__(self, model: EmbeddingModel, similarity_metric: SimilarityMetric): + self.model = model + self.similarity_metric = similarity_metric + + def get_embedding(self, text: str) -> np.ndarray: + try: + embedding = self.model.encode(text) + logger.info(f"Generated embedding for text: {text[:30]}...") + return embedding + except Exception as e: + logger.error(f"Failed to generate embedding: {e}") + raise + + def compare_embeddings(self, text_a: str, text_b: str) -> float: + try: + emb_a = self.get_embedding(text_a) + emb_b = self.get_embedding(text_b) + similarity = self.similarity_metric.compute(emb_a, emb_b) + logger.info(f"Computed similarity: {similarity}") + return similarity + except Exception as e: + logger.error(f"Failed to compare embeddings: {e}") + raise diff --git a/server/app/core/services/llm_service.py b/server/app/core/services/llm_service.py index cd06356..6dfd320 100644 --- a/server/app/core/services/llm_service.py +++ b/server/app/core/services/llm_service.py @@ -1,8 +1,8 @@ from ...config import Config -from ...utils import get_response_claude, get_response_openai +from ...utils.api_clients import ClaudeClient, OpenAIClient from ...utils.model_map import get_model_type -response_function_map = {"OpenAI": get_response_openai, "Anthropic": get_response_claude} +llm_clients = {"OpenAI": OpenAIClient, "Anthropic": ClaudeClient} class LLMResponseService: @@ -12,13 +12,15 @@ def __init__( prompt: list[dict], max_tokens: int = Config.DEFAULT_MAX_TOKENS, temperature: float = Config.DEFAULT_TEMPERATURE, + stream: bool = False, ) -> None: self.model_type = get_model_type(model) - self.response_function = response_function_map[self.model_type] + self.llm_client = llm_clients.get(self.model_type) self.model = model self.prompt = prompt self.max_tokens = max_tokens self.temperature = temperature + self.stream = stream self._validate_prompt() def _validate_prompt(self): @@ -34,17 +36,11 @@ def _validate_prompt(self): if "content" not in p: raise ValueError("Prompt should have a content key") - def get_streaming_response(self): - response = self.response_function( - prompt=self.prompt, model=self.model, temp=self.temperature, max_tokens=self.max_tokens, stream=True - ) - full_response = "" - for chunk in response: - full_response += chunk - yield chunk - def get_response(self): - response = self.response_function( - prompt=self.prompt, model=self.model, temp=self.temperature, max_tokens=self.max_tokens, stream=False + return self.llm_client().get_response( + prompt=self.prompt, + model=self.model, + temperature=self.temperature, + max_tokens=self.max_tokens, + stream=self.stream, ) - return response diff --git a/server/app/utils/api_clients.py b/server/app/utils/api_clients.py index 0a5adeb..ea06456 100644 --- a/server/app/utils/api_clients.py +++ b/server/app/utils/api_clients.py @@ -4,56 +4,67 @@ from ..config import Config -def get_response_claude_stream(message): - """Helper function to handle streaming responses""" - for response in message: - if response.type == "content_block_delta": - yield response.delta.text - - -def get_response_claude( - prompt: list, - model: str = Config.DEFAULT_CLAUDE_MODEL, - temp: float = Config.DEFAULT_TEMPERATURE, - max_tokens: int = Config.DEFAULT_MAX_TOKENS, - stream: bool = False, -): - client = Anthropic(api_key=Config.CLAUDE_API_KEY) - message = client.messages.create( - model=model, - max_tokens=max_tokens, - temperature=temp, - system=prompt[0]["content"], - messages=prompt[1:], - stream=stream, - ) - - if stream: - return get_response_claude_stream(message) - - return message.content[0].text - - -def get_response_openai_stream(completion): - """Helper function to handle OpenAI streaming responses""" - for response in completion: - yield response.choices[0].delta.content or "" - - -def get_response_openai( - prompt: list, - model: str = Config.DEFAULT_OPENAI_MODEL, - max_tokens: int = Config.DEFAULT_MAX_TOKENS, - temp: float = Config.DEFAULT_TEMPERATURE, - stream: bool = False, -): - client = OpenAI(api_key=Config.OPENAI_API_KEY) - - completion = client.chat.completions.create( - model=model, messages=prompt, temperature=temp, max_tokens=max_tokens, stream=stream - ) - - if stream: - return get_response_openai_stream(completion) - - return completion.choices[0].message.content +class ClaudeClient: + def __init__(self, api_key: str): + self.api_key = api_key + if not self.api_key: + self.api_key = Config.CLAUDE_API_KEY + self.client = Anthropic(api_key=self.api_key) + + def get_response_stream(self, message): + """Helper function to handle streaming responses""" + for response in message: + if response.type == "content_block_delta": + yield response.delta.text + + def get_response( + self, + prompt: list, + model: str = Config.DEFAULT_CLAUDE_MODEL, + temperature: float = Config.DEFAULT_TEMPERATURE, + max_tokens: int = Config.DEFAULT_MAX_TOKENS, + stream: bool = False, + ): + message = self.client.messages.create( + model=model, + max_tokens=max_tokens, + temperature=temperature, + system=prompt[0]["content"], + messages=prompt[1:], + stream=stream, + ) + + if stream: + return self.get_response_stream(message) + + return message.content[0].text + + +class OpenAIClient: + def __init__(self, api_key: str): + self.api_key = api_key + if not self.api_key: + self.api_key = Config.OPENAI_API_KEY + self.client = OpenAI(api_key=self.api_key) + + def _get_response_stream(self, completion): + """Helper function to handle OpenAI streaming responses""" + for response in completion: + yield response.choices[0].delta.content or "" + + def get_response( + self, + prompt: list, + model: str = Config.DEFAULT_OPENAI_MODEL, + max_tokens: int = Config.DEFAULT_MAX_TOKENS, + temperature: float = Config.DEFAULT_TEMPERATURE, + stream: bool = False, + ): + completion = self.client.chat.completions.create( + model=model, messages=prompt, temperature=temperature, max_tokens=max_tokens, stream=stream + ) + + if stream: + return self._get_response_stream(completion) + + return completion.choices[0].message.content From af0137f4c6965ab0d11ba0aa24ecb3bab5aeb94b Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:29:09 +0530 Subject: [PATCH 52/62] minor change --- server/app/utils/api_clients.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/app/utils/api_clients.py b/server/app/utils/api_clients.py index ea06456..e1e6c88 100644 --- a/server/app/utils/api_clients.py +++ b/server/app/utils/api_clients.py @@ -11,7 +11,7 @@ def __init__(self, api_key: str): self.api_key = Config.CLAUDE_API_KEY self.client = Anthropic(api_key=self.api_key) - def get_response_stream(self, message): + def _get_response_stream(self, message): """Helper function to handle streaming responses""" for response in message: if response.type == "content_block_delta": @@ -35,7 +35,7 @@ def get_response( ) if stream: - return self.get_response_stream(message) + return self._get_response_stream(message) return message.content[0].text From 2f6a213bf6cb65088a36957b1f598fba7e1daa77 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:34:57 +0530 Subject: [PATCH 53/62] added embedding service with client --- server/app/config.py | 2 ++ server/app/core/services/embedding_service.py | 20 +++++++-------- server/app/utils/api_clients.py | 25 +++++++++++++++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/server/app/config.py b/server/app/config.py index c8d46c6..ae14a0b 100644 --- a/server/app/config.py +++ b/server/app/config.py @@ -33,6 +33,8 @@ class Config: # Model defaults DEFAULT_CLAUDE_MODEL = "claude-3-5-sonnet-20240620" DEFAULT_OPENAI_MODEL = "gpt-4" + DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small" + DEFAULT_CLAUDE_EMBEDDING_MODEL = "claude-3-5-sonnet-20241022" DEFAULT_MAX_TOKENS = 4096 DEFAULT_TEMPERATURE = 0.7 diff --git a/server/app/core/services/embedding_service.py b/server/app/core/services/embedding_service.py index fef4365..7efdea0 100644 --- a/server/app/core/services/embedding_service.py +++ b/server/app/core/services/embedding_service.py @@ -1,8 +1,8 @@ from abc import ABC, abstractmethod import numpy as np -from anthropic import Anthropic -from openai import OpenAI + +from server.app.utils.api_clients import ClaudeClient, OpenAIClient from ...config import Config, Logging @@ -22,21 +22,19 @@ def encode(self, text: str) -> np.ndarray: class OpenAIEmbeddingModel(EmbeddingModel): def __init__(self, model_name: str): self.model_name = model_name - self.client = OpenAI(api_key=Config.OPENAI_API_KEY) + self.client = OpenAIClient() def encode(self, text: str) -> np.ndarray: - response = self.client.embeddings.create(input=text, model=self.model_name) - return np.array(response.data[0].embedding) + return self.client.get_embeddings(text, self.model_name) class ClaudeEmbeddingModel(EmbeddingModel): def __init__(self, model_name: str): self.model_name = model_name - self.client = Anthropic(api_key=Config.CLAUDE_API_KEY) + self.client = ClaudeClient() def encode(self, text: str) -> np.ndarray: - response = self.client.embeddings.create(input=text, model=self.model_name) - return np.array(response.data[0].embedding) + return self.client.get_embeddings(text, self.model_name) class SimilarityMetric(ABC): @@ -77,10 +75,10 @@ def get_embedding(self, text: str) -> np.ndarray: logger.error(f"Failed to generate embedding: {e}") raise - def compare_embeddings(self, text_a: str, text_b: str) -> float: + def get_embedding_similarity(self, text_a: str, text_b: str) -> float: try: - emb_a = self.get_embedding(text_a) - emb_b = self.get_embedding(text_b) + emb_a = self.model.encode(text_a) + emb_b = self.model.encode(text_b) similarity = self.similarity_metric.compute(emb_a, emb_b) logger.info(f"Computed similarity: {similarity}") return similarity diff --git a/server/app/utils/api_clients.py b/server/app/utils/api_clients.py index e1e6c88..cab9b11 100644 --- a/server/app/utils/api_clients.py +++ b/server/app/utils/api_clients.py @@ -1,10 +1,23 @@ +from abc import ABC, abstractmethod + +import numpy as np from anthropic import Anthropic from openai import OpenAI from ..config import Config -class ClaudeClient: +class BaseClient(ABC): + @abstractmethod + def get_response(self, prompt: list, model: str, temperature: float, max_tokens: int, stream: bool): + pass + + @abstractmethod + def get_embeddings(self, input: str, model: str): + pass + + +class ClaudeClient(BaseClient): def __init__(self, api_key: str): self.api_key = api_key if not self.api_key: @@ -39,8 +52,12 @@ def get_response( return message.content[0].text + def get_embeddings(self, input: str, model: str = Config.DEFAULT_CLAUDE_EMBEDDING_MODEL): + response = self.client.embeddings.create(input=input, model=model) + return np.array(response.data[0].embedding) + -class OpenAIClient: +class OpenAIClient(BaseClient): def __init__(self, api_key: str): self.api_key = api_key if not self.api_key: @@ -68,3 +85,7 @@ def get_response( return self._get_response_stream(completion) return completion.choices[0].message.content + + def get_embeddings(self, input: str, model: str = Config.DEFAULT_OPENAI_EMBEDDING_MODEL): + response = self.client.embeddings.create(input=input, model=model) + return np.array(response.data[0].embedding) From 89a58394551b91c8d9dac3bc8ba5c71861f48b54 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:43:48 +0530 Subject: [PATCH 54/62] more modular embedding service --- server/app/core/services/embedding_service.py | 2 +- .../app/core/services/evaluation_services.py | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/server/app/core/services/embedding_service.py b/server/app/core/services/embedding_service.py index 7efdea0..3d25c56 100644 --- a/server/app/core/services/embedding_service.py +++ b/server/app/core/services/embedding_service.py @@ -52,7 +52,7 @@ def compute(self, vector_a: np.ndarray, vector_b: np.ndarray) -> float: return np.dot(vector_a, vector_b) / (np.linalg.norm(vector_a) * np.linalg.norm(vector_b) + 1e-8) -class EuclideanDistance(SimilarityMetric): +class EuclideanSimilarity(SimilarityMetric): def compute(self, vector_a: np.ndarray, vector_b: np.ndarray) -> float: return np.linalg.norm(vector_a - vector_b) diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index f08fc55..d40983c 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -3,11 +3,25 @@ import numpy as np from fuzzywuzzy import fuzz +from server.app.core.services.embedding_service import ( + ClaudeEmbeddingModel, + CosineSimilarity, + EmbeddingService, + EuclideanSimilarity, + OpenAIEmbeddingModel, +) + from ...config import Config, Logging from ..services.llm_service import LLMResponseService logger = Logging.get_logger(__name__) +# Embedding models +embedding_model_map = {"OpenAI": OpenAIEmbeddingModel, "Anthropic": ClaudeEmbeddingModel} + +# Similarity metrics +similarity_metric_map = {"cosine": CosineSimilarity, "euclidean": EuclideanSimilarity} + class EvaluationStrategy(ABC): """ @@ -134,23 +148,14 @@ class EmbeddingEvaluationStrategy(EvaluationStrategy): Supports multiple similarity metrics. """ - def __init__(self, embedding_model, similarity_metric: str = "cosine"): - self.embedding_model = embedding_model - self.similarity_metric = similarity_metric + def __init__(self, embedding_model: str, similarity_metric: str = "cosine"): + self.embedding_model = embedding_model_map.get(embedding_model) + self.similarity_metric = similarity_metric_map.get(similarity_metric) + self.embedding_service = EmbeddingService(self.embedding_model, self.similarity_metric) def evaluate(self, input_text: str, output_text: str, **kwargs): try: - input_emb = self.embedding_model.encode(input_text) - output_emb = self.embedding_model.encode(output_text) - - if self.similarity_metric == "cosine": - similarity = np.dot(input_emb, output_emb) / ( - np.linalg.norm(input_emb) * np.linalg.norm(output_emb) + 1e-8 - ) - elif self.similarity_metric == "euclidean": - similarity = np.linalg.norm(input_emb - output_emb) - else: - raise ValueError(f"Unsupported similarity metric: {self.similarity_metric}") + similarity = self.embedding_service.get_embedding_similarity(input_text, output_text) return similarity except Exception as e: logger.error(f"Error in embedding evaluation: {e}") From 0afa3f4c8041d43cc332b4eef02fb7308b2e2f2b Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:45:20 +0530 Subject: [PATCH 55/62] default addition --- server/app/api_views/evaluation_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/api_views/evaluation_view.py b/server/app/api_views/evaluation_view.py index ea343e5..0c2936e 100644 --- a/server/app/api_views/evaluation_view.py +++ b/server/app/api_views/evaluation_view.py @@ -40,7 +40,7 @@ def post(self, project_id, agent_id): evaluator_strategy = LLMEvaluationStrategy(config=evaluator_config, guidelines=base_guidelines) elif strategy_type == "embedding": - embedding_model = evaluator_config.get("embedding_model") + embedding_model = evaluator_config.get("embedding_model", "OpenAI") similarity_metric = evaluator_config.get("similarity_metric", "cosine") if not embedding_model: return jsonify({"error": "Embedding model not provided in evaluator config"}), 400 From bd5acaf35276ba747fc8f752224a11010757df3c Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:47:38 +0530 Subject: [PATCH 56/62] import fixes --- server/app/core/services/embedding_service.py | 3 +-- server/app/core/services/evaluation_services.py | 6 ++---- server/app/utils/__init__.py | 4 ---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/server/app/core/services/embedding_service.py b/server/app/core/services/embedding_service.py index 3d25c56..38d414c 100644 --- a/server/app/core/services/embedding_service.py +++ b/server/app/core/services/embedding_service.py @@ -2,9 +2,8 @@ import numpy as np -from server.app.utils.api_clients import ClaudeClient, OpenAIClient - from ...config import Config, Logging +from ...utils.api_clients import ClaudeClient, OpenAIClient logger = Logging.get_logger(__name__) diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index d40983c..571facb 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -1,17 +1,15 @@ from abc import ABC, abstractmethod -import numpy as np from fuzzywuzzy import fuzz -from server.app.core.services.embedding_service import ( +from ...config import Config, Logging +from ..services.embedding_service import ( ClaudeEmbeddingModel, CosineSimilarity, EmbeddingService, EuclideanSimilarity, OpenAIEmbeddingModel, ) - -from ...config import Config, Logging from ..services.llm_service import LLMResponseService logger = Logging.get_logger(__name__) diff --git a/server/app/utils/__init__.py b/server/app/utils/__init__.py index 24e7f2a..e69de29 100644 --- a/server/app/utils/__init__.py +++ b/server/app/utils/__init__.py @@ -1,4 +0,0 @@ -from .api_clients import get_response_claude, get_response_openai -from .prompts import system_score - -__all__ = ["get_response_claude", "get_response_openai", "system_score"] From 9e7480be40c0d9c0a8cf7b2a573f6b732223ed1b Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:55:55 +0530 Subject: [PATCH 57/62] fixed embedding evaluation service --- server/app/core/services/embedding_service.py | 4 ++-- server/app/core/services/evaluation_services.py | 4 ++-- server/app/utils/api_clients.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/app/core/services/embedding_service.py b/server/app/core/services/embedding_service.py index 38d414c..c2c076e 100644 --- a/server/app/core/services/embedding_service.py +++ b/server/app/core/services/embedding_service.py @@ -19,7 +19,7 @@ def encode(self, text: str) -> np.ndarray: class OpenAIEmbeddingModel(EmbeddingModel): - def __init__(self, model_name: str): + def __init__(self, model_name: str = Config.DEFAULT_OPENAI_EMBEDDING_MODEL): self.model_name = model_name self.client = OpenAIClient() @@ -28,7 +28,7 @@ def encode(self, text: str) -> np.ndarray: class ClaudeEmbeddingModel(EmbeddingModel): - def __init__(self, model_name: str): + def __init__(self, model_name: str = Config.DEFAULT_CLAUDE_EMBEDDING_MODEL): self.model_name = model_name self.client = ClaudeClient() diff --git a/server/app/core/services/evaluation_services.py b/server/app/core/services/evaluation_services.py index 571facb..04228ee 100644 --- a/server/app/core/services/evaluation_services.py +++ b/server/app/core/services/evaluation_services.py @@ -147,8 +147,8 @@ class EmbeddingEvaluationStrategy(EvaluationStrategy): """ def __init__(self, embedding_model: str, similarity_metric: str = "cosine"): - self.embedding_model = embedding_model_map.get(embedding_model) - self.similarity_metric = similarity_metric_map.get(similarity_metric) + self.embedding_model = embedding_model_map.get(embedding_model)() + self.similarity_metric = similarity_metric_map.get(similarity_metric)() self.embedding_service = EmbeddingService(self.embedding_model, self.similarity_metric) def evaluate(self, input_text: str, output_text: str, **kwargs): diff --git a/server/app/utils/api_clients.py b/server/app/utils/api_clients.py index cab9b11..5ac1a73 100644 --- a/server/app/utils/api_clients.py +++ b/server/app/utils/api_clients.py @@ -18,7 +18,7 @@ def get_embeddings(self, input: str, model: str): class ClaudeClient(BaseClient): - def __init__(self, api_key: str): + def __init__(self, api_key: str = None): self.api_key = api_key if not self.api_key: self.api_key = Config.CLAUDE_API_KEY @@ -58,7 +58,7 @@ def get_embeddings(self, input: str, model: str = Config.DEFAULT_CLAUDE_EMBEDDIN class OpenAIClient(BaseClient): - def __init__(self, api_key: str): + def __init__(self, api_key: str = None): self.api_key = api_key if not self.api_key: self.api_key = Config.OPENAI_API_KEY From 6b1ccfc442f52e82cf1e79c982f5324c38047bdd Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 15:58:10 +0530 Subject: [PATCH 58/62] minor fix --- server/app/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/__init__.py b/server/app/__init__.py index 9fe3e58..2524edd 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -22,7 +22,6 @@ def register_api_blueprints(app): api_bp = Blueprint("api", __name__, url_prefix="/api") api_bp.register_blueprint(config_bp) - api_bp.register_blueprint(main_bp) api_bp.register_blueprint(auth_bp) api_bp.register_blueprint(project_bp) api_bp.register_blueprint(evaluation_bp) @@ -30,6 +29,7 @@ def register_api_blueprints(app): api_bp.register_blueprint(ai_agent_bp) api_bp.register_blueprint(agent_variant_bp) app.register_blueprint(api_bp) + app.register_blueprint(main_bp) def create_app(config_class=Config): From 6c54f1a74cfafde600dfb66685712c62d9a94e8e Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 16:11:20 +0530 Subject: [PATCH 59/62] Added Readme --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0e46d3..7ee5ad3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,69 @@ -# Steps to install precommit +# ProBack Backend + +## Technologies Used + +- **Flask**: Web framework for building the API. +- **MongoEngine**: ODM for MongoDB. +- **Firebase Admin SDK**: For authentication. +- **Marshmallow-MongoEngine**: For serialization. + +## Installation + +### Prerequisites + +- Python 3.12 +- MongoDB Instance +- Firebase Project with service account credentials +- Azure Account for deployment + +### Steps + +1. **Clone the Repository** + + ```bash + git clone + cd proback/server + ``` + +2. **Create Virtual Environment** + + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +3. **Install Dependencies** + + ```bash + pip install -r requirements.txt + ``` + +4. **Configure Environment Variables** + + Create a `.env` file in the `server` directory with the following: + + ```env + MONGO_URI=your_mongodb_uri + SECRET_KEY=your_secret_key + CLAUDE_API_KEY=your_claude_api_key + OPENAI_API_KEY=your_openai_api_key + ``` + +5. **Firebase Setup** + + Place your `firebase-credentials.json` in the project root as specified in `config.py`. + +## Configuration + +The backend configuration is managed through the `Config` class in `server/app/config.py`. It loads environment variables and sets default values for various settings. + +### CORS + +Configure allowed origins in the `CORS_ORIGINS` list within `config.py`. + + +### Setup Pre-commit + 1. change directory to server and do `pip install -r requirements.txt` 2. now run `pre-commit install` to install precommit hooks -3. now you can run `pre-commit run --all-files` to run precommit hooks on all files if you want to run on specific file then run `pre-commit run ` \ No newline at end of file +3. now you can run `pre-commit run --all-files` to run precommit hooks on all files if you want to run on specific file then run `pre-commit run ` From 94e329ceef30b7389966eb35ed2c58e9ef75ac0a Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 18:48:19 +0530 Subject: [PATCH 60/62] updated readme --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 7ee5ad3..7614692 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,5 @@ # ProBack Backend -## Technologies Used - -- **Flask**: Web framework for building the API. -- **MongoEngine**: ODM for MongoDB. -- **Firebase Admin SDK**: For authentication. -- **Marshmallow-MongoEngine**: For serialization. - ## Installation ### Prerequisites @@ -14,7 +7,6 @@ - Python 3.12 - MongoDB Instance - Firebase Project with service account credentials -- Azure Account for deployment ### Steps From 350d4a2736eec85d1ce078bb4956192c2d3ca0b3 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 18:49:54 +0530 Subject: [PATCH 61/62] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7614692..1dadd5a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ```bash python -m venv venv - source venv/bin/activate # On Windows: venv\Scripts\activate + source venv/bin/activate ``` 3. **Install Dependencies** From f42184ab8dd381ae4d8dec7835173625bb436485 Mon Sep 17 00:00:00 2001 From: bhaveshsonalkar Date: Sun, 2 Feb 2025 21:06:00 +0530 Subject: [PATCH 62/62] fixes in streaming response service --- server/app/api_views/ai_agent_view.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/app/api_views/ai_agent_view.py b/server/app/api_views/ai_agent_view.py index 3713cb2..58b7913 100644 --- a/server/app/api_views/ai_agent_view.py +++ b/server/app/api_views/ai_agent_view.py @@ -112,19 +112,17 @@ def post(self): data = request.json config = data.get("config") prompt = data.get("prompt") - stream_enabled = data.get("stream", True) - logger.info( - f"Generating conversation for prompt: {prompt} with config: {config} and stream: {stream_enabled}" - ) + stream = data.get("stream", True) + logger.info(f"Generating conversation for prompt: {prompt} with config: {config} and stream: {stream}") model = config.get("model") max_tokens = config.get("max_tokens") or Config.DEFAULT_MAX_TOKENS temperature = config.get("temperature") or Config.DEFAULT_TEMPERATURE - llm_service = LLMResponseService(model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature) - if stream_enabled: + llm_service = LLMResponseService( + model=model, prompt=prompt, max_tokens=max_tokens, temperature=temperature, stream=stream + ) + if stream: logger.info(f"Streaming conversation for prompt: {prompt}") - return Response( - stream_with_context(llm_service.get_streaming_response()), content_type="text/event-stream" - ) + return Response(stream_with_context(llm_service.get_response()), content_type="text/event-stream") else: logger.info(f"Generating conversation for prompt: {prompt} without stream") return jsonify(llm_service.get_response()), 200