sahilmayekar commited on
Commit
e7addf3
·
1 Parent(s): 16eff4c
.gitignore CHANGED
@@ -1 +1,2 @@
1
- /python
 
 
1
+ /python
2
+ /frontend/node_modules
Dockerfile CHANGED
@@ -41,6 +41,6 @@ ENV PORT=7860
41
 
42
  EXPOSE 11434 7860 5000
43
 
44
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
45
 
46
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
 
41
 
42
  EXPOSE 11434 7860 5000
43
 
44
+ HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health || exit 1
45
 
46
  ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
crew_result.txt ADDED
File without changes
entrypoint.sh CHANGED
@@ -9,6 +9,6 @@ done
9
 
10
  ollama pull nomic-embed-text:latest
11
 
12
- ollama pull gpt-oss:20b || true
13
 
14
  exec flask --app src/app run --host=0.0.0.0 --port=$PORT
 
9
 
10
  ollama pull nomic-embed-text:latest
11
 
12
+ ollama pull llama3.1:8b-instruct-q4_0 || true
13
 
14
  exec flask --app src/app run --host=0.0.0.0 --port=$PORT
frontend/app/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/app/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/app/components.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": false,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/index.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
frontend/app/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs['recommended-latest'],
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend/app/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>app</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/app/jsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ }
7
+ }
8
+ }
frontend/app/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/app/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@monaco-editor/react": "^4.7.0",
14
+ "@radix-ui/react-slot": "^1.2.3",
15
+ "@tailwindcss/vite": "^4.1.13",
16
+ "class-variance-authority": "^0.7.1",
17
+ "clsx": "^2.1.1",
18
+ "lucide-react": "^0.544.0",
19
+ "monaco-editor": "^0.53.0",
20
+ "react": "^19.1.1",
21
+ "react-dom": "^19.1.1",
22
+ "socket.io-client": "^4.8.1",
23
+ "tailwind-merge": "^3.3.1",
24
+ "tailwind-variants": "^3.1.1",
25
+ "tailwindcss": "^4.1.13",
26
+ "zustand": "^5.0.8"
27
+ },
28
+ "devDependencies": {
29
+ "@eslint/js": "^9.36.0",
30
+ "@types/react": "^19.1.13",
31
+ "@types/react-dom": "^19.1.9",
32
+ "@vitejs/plugin-react": "^5.0.3",
33
+ "eslint": "^9.36.0",
34
+ "eslint-plugin-react-hooks": "^5.2.0",
35
+ "eslint-plugin-react-refresh": "^0.4.20",
36
+ "globals": "^16.4.0",
37
+ "tw-animate-css": "^1.3.8",
38
+ "vite": "^7.1.7"
39
+ }
40
+ }
frontend/app/public/vite.svg ADDED
frontend/app/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/app/src/App.jsx ADDED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef } from "react";
2
+ import { Editor, DiffEditor } from "@monaco-editor/react";
3
+ import { create } from "zustand";
4
+ import { io } from "socket.io-client";
5
+
6
+ const BASE_URL = "http://localhost:7860";
7
+
8
+ const useStore = create((set, get) => ({
9
+ files: [],
10
+ selectedFile: null,
11
+ editorContent: "",
12
+ dbSchema: { tables: [] },
13
+ chat: [],
14
+ consoleOutput: "",
15
+ loading: false,
16
+ fileProposals: {},
17
+ showDiffForFile: null,
18
+ fileContents: {},
19
+ setFiles: (files) => set({ files }),
20
+ setSelectedFile: (name) => set({ selectedFile: { name } }),
21
+ setEditorContent: (editorContent) => set({ editorContent }),
22
+ setDbSchema: (dbSchema) => set({ dbSchema }),
23
+ pushChat: (msg) => set((s) => ({ chat: [...s.chat, msg] })),
24
+ setConsoleOutput: (consoleOutput) => set({ consoleOutput }),
25
+ setLoading: (loading) => set({ loading }),
26
+ setFileProposal: (name, details) => set((s) => ({ fileProposals: { ...s.fileProposals, [name]: { ...details, active: true } } })),
27
+ setShowDiffForFile: (fileName) => set({ showDiffForFile: fileName }),
28
+ openFile: async (name) => {
29
+ const currentState = get();
30
+ if (currentState.selectedFile?.name && currentState.editorContent) {
31
+ set({ fileContents: { ...currentState.fileContents, [currentState.selectedFile.name]: currentState.editorContent } });
32
+ }
33
+ set({ loading: true });
34
+ let content = "";
35
+ const state = get();
36
+ const proposal = state.fileProposals[name];
37
+ if (proposal?.isNew) {
38
+ content = "";
39
+ } else {
40
+ content = state.fileContents[name];
41
+ if (!content) {
42
+ try {
43
+ const res = await fetch(`${BASE_URL}/files/${encodeURIComponent(name)}`);
44
+ if (!res.ok) throw new Error("Failed to load file");
45
+ const data = await res.json();
46
+ content = data.content;
47
+ set({ fileContents: { ...get().fileContents, [name]: content } });
48
+ } catch (e) {
49
+ console.warn(e);
50
+ content = `-- demo content for ${name}\nSELECT 1;`;
51
+ set({ fileContents: { ...get().fileContents, [name]: content } });
52
+ }
53
+ }
54
+ }
55
+ set({ selectedFile: { name }, editorContent: content, loading: false });
56
+ const finalState = get();
57
+ const prop = finalState.fileProposals[name];
58
+ if (prop?.active) {
59
+ set({ showDiffForFile: name });
60
+ } else {
61
+ set({ showDiffForFile: null });
62
+ }
63
+ },
64
+ acceptProposal: () => {
65
+ const state = get();
66
+ const file = state.selectedFile?.name;
67
+ if (state.showDiffForFile !== file) return;
68
+ const prop = state.fileProposals[file];
69
+ if (!prop) return;
70
+ if (prop.isNew) {
71
+ const confirmedName = prompt("Save new file as (.sql required):", prop.suggestedName);
72
+ if (!confirmedName?.trim() || !confirmedName.endsWith(".sql")) {
73
+ return;
74
+ }
75
+ const finalName = confirmedName.trim();
76
+ fetch(`${BASE_URL}/files`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({ filename: finalName, content: prop.code }),
80
+ })
81
+ .then((res) => {
82
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
83
+ return res.json();
84
+ })
85
+ .then(() => {
86
+ const newProps = { ...state.fileProposals };
87
+ delete newProps[file];
88
+ const currentFiles = get().files;
89
+ set({
90
+ files: [...currentFiles, { name: finalName }],
91
+ editorContent: prop.code,
92
+ fileContents: { ...get().fileContents, [finalName]: prop.code },
93
+ showDiffForFile: null,
94
+ fileProposals: newProps,
95
+ });
96
+ if (finalName !== file) {
97
+ set({ selectedFile: { name: finalName } });
98
+ }
99
+ })
100
+ .catch((e) => console.error("Failed to create file:", e));
101
+ } else {
102
+ fetch(`${BASE_URL}/files/${encodeURIComponent(file)}`, {
103
+ method: "PUT",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: JSON.stringify({ content: prop.code }),
106
+ })
107
+ .then((res) => {
108
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
109
+ const newProps = { ...state.fileProposals };
110
+ delete newProps[file];
111
+ set({
112
+ editorContent: prop.code,
113
+ fileContents: { ...state.fileContents, [file]: prop.code },
114
+ showDiffForFile: null,
115
+ fileProposals: newProps,
116
+ });
117
+ })
118
+ .catch((e) => console.error("Failed to save after accept:", e));
119
+ }
120
+ },
121
+ rejectProposal: () => {
122
+ const state = get();
123
+ const file = state.selectedFile?.name;
124
+ if (state.showDiffForFile !== file) return;
125
+ const newProps = { ...state.fileProposals };
126
+ delete newProps[file];
127
+ set({ showDiffForFile: null, fileProposals: newProps });
128
+ },
129
+ refreshFiles: async () => {
130
+ try {
131
+ const res = await fetch(`${BASE_URL}/files`);
132
+ if (!res.ok) throw new Error("Failed to load files");
133
+ let data = await res.json();
134
+ data = data.filter((f) => f.name.endsWith(".sql"));
135
+ set({ files: data || [] });
136
+ } catch (e) {
137
+ console.warn(e);
138
+ }
139
+ },
140
+ }));
141
+
142
+ const Card = ({ children, className = "" }) => (
143
+ <div className={`rounded-xl border bg-white shadow-sm ${className}`}>{children}</div>
144
+ );
145
+ const CardContent = ({ children, className = "" }) => (
146
+ <div className={`p-3 ${className}`}>{children}</div>
147
+ );
148
+ const IconButton = ({ children, onClick, className = "" }) => (
149
+ <button
150
+ onClick={onClick}
151
+ className={`px-3 py-1 rounded-md border hover:shadow-sm text-sm bg-blue-500 text-white ${className}`}
152
+ >
153
+ {children}
154
+ </button>
155
+ );
156
+
157
+ function FileExplorer() {
158
+ const { files, selectedFile, loading, fileProposals, fileContents, editorContent } = useStore();
159
+ const openFile = useStore((s) => s.openFile);
160
+
161
+ const loadFiles = async () => {
162
+ useStore.setState({ loading: true });
163
+ try {
164
+ const res = await fetch(`${BASE_URL}/files`);
165
+ if (!res.ok) throw new Error("Failed to load files");
166
+ let data = await res.json();
167
+ data = data.filter((f) => f.name.endsWith(".sql"));
168
+ useStore.setState({ files: data || [] });
169
+ } catch (e) {
170
+ console.warn(e);
171
+ } finally {
172
+ useStore.setState({ loading: false });
173
+ }
174
+ };
175
+
176
+ useEffect(() => {
177
+ loadFiles();
178
+ }, []);
179
+
180
+ useEffect(() => {
181
+ if (files.length > 0 && !selectedFile?.name) {
182
+ openFile(files[0].name);
183
+ }
184
+ }, [files, selectedFile?.name, openFile]);
185
+
186
+ async function createNewFile() {
187
+ const name = prompt("New file name (must end with .sql):");
188
+ if (!name || !name.endsWith(".sql")) return;
189
+ try {
190
+ await fetch(`${BASE_URL}/files`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" },
193
+ body: JSON.stringify({ filename: name, content: "" }),
194
+ });
195
+ loadFiles();
196
+ } catch (e) {
197
+ console.warn(e);
198
+ }
199
+ }
200
+
201
+ const proposedNews = Object.entries(fileProposals).filter(([n, p]) => p.isNew);
202
+
203
+ return (
204
+ <Card className="h-full flex flex-col">
205
+ <CardContent className="flex items-center justify-between">
206
+ <h3 className="text-sm font-semibold">Files</h3>
207
+ <div className="flex gap-2">
208
+ <IconButton onClick={createNewFile}>New</IconButton>
209
+ </div>
210
+ </CardContent>
211
+
212
+ <div className="px-3 pb-3 overflow-auto">
213
+ <ul className="space-y-1">
214
+ {files.map((f) => {
215
+ const hasProposal = fileProposals[f.name]?.active;
216
+ const isDirty = f.name === selectedFile?.name && (!fileContents[f.name] || editorContent !== fileContents[f.name]);
217
+ return (
218
+ <li key={f.name}>
219
+ <button
220
+ onClick={() => openFile(f.name)}
221
+ className={`w-full text-left px-2 py-1 rounded-md text-white hover:bg-slate-50 ${selectedFile?.name === f.name ? "bg-slate-100 font-medium" : ""}`}
222
+ >
223
+ <div className="flex items-center justify-between">
224
+ <span>{f.name}{isDirty ? " *" : ""}</span>
225
+ {hasProposal && <div className="w-2 h-2 bg-green-500 rounded-full"></div>}
226
+ </div>
227
+ </button>
228
+ </li>
229
+ );
230
+ })}
231
+ {proposedNews.map(([name]) => {
232
+ const prop = fileProposals[name];
233
+ const isDirty = name === selectedFile?.name && (!fileContents[name] || editorContent !== fileContents[name]);
234
+ return (
235
+ <li key={`p-${name}`}>
236
+ <button
237
+ onClick={() => openFile(name)}
238
+ className={`w-full text-left px-2 py-1 rounded-md text-white hover:bg-slate-50 ${selectedFile?.name === name ? "bg-slate-100 font-medium" : ""}`}
239
+ >
240
+ <div className="flex items-center justify-between">
241
+ <span className="italic text-blue-600">{name} (proposed){isDirty ? " *" : ""}</span>
242
+ <div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
243
+ </div>
244
+ </button>
245
+ </li>
246
+ );
247
+ })}
248
+ </ul>
249
+ </div>
250
+
251
+ <div className="mt-auto p-3 border-t">
252
+ <div className="text-xs text-slate-500">App Status: {loading ? "loading" : "ready"}</div>
253
+ </div>
254
+ </Card>
255
+ );
256
+ }
257
+
258
+ function DBViewer() {
259
+ const { dbSchema, setDbSchema, setConsoleOutput, setFileProposal, setShowDiffForFile} = useStore();
260
+
261
+ useEffect(() => {
262
+ async function loadSchema() {
263
+ try {
264
+ const tres = await fetch(`${BASE_URL}/tables`);
265
+ if (!tres.ok) throw new Error("no tables");
266
+ const tables = await tres.json();
267
+ const tablesWithCols = await Promise.all(
268
+ tables.map(async (t) => {
269
+ const qres = await fetch(`${BASE_URL}/query`, {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({ query: `PRAGMA table_info(${t})` }),
273
+ });
274
+ const colsData = await qres.json();
275
+ console.log(colsData)
276
+ const columns = colsData.map((c) => `${c.name} ${c.type}`);
277
+ return { name: t, columns };
278
+ })
279
+ );
280
+ setDbSchema({ tables: tables.map((t) => ({ name: t, columns: [] })) });
281
+ } catch (e) {
282
+ }
283
+ }
284
+ loadSchema();
285
+ }, [setDbSchema]);
286
+
287
+ async function inspectTable(table) {
288
+ try {
289
+ const res = await fetch(`${BASE_URL}/query`, {
290
+ method: "POST",
291
+ headers: { "Content-Type": "application/json" },
292
+ body: JSON.stringify({ query: `SELECT * FROM ${table} LIMIT 50;` }),
293
+ });
294
+ const data = await res.json();
295
+ setConsoleOutput(JSON.stringify(data, null, 2));
296
+ } catch (e) {
297
+ setConsoleOutput(`Failed to query table ${table}: ${e.message}`);
298
+ }
299
+ }
300
+
301
+ return (
302
+ <Card className="h-full">
303
+ <CardContent>
304
+ <div className="flex items-center justify-between">
305
+ <h3 className="text-sm font-semibold">DB Viewer</h3>
306
+ <div className="text-xs text-slate-500">SQLite</div>
307
+ </div>
308
+
309
+ <div className="mt-3 space-y-3 overflow-auto" style={{ maxHeight: "40vh" }}>
310
+ {dbSchema.tables.map((t) => (
311
+ <div key={t.name} className="p-2 rounded border hover:bg-slate-50">
312
+ <div className="flex justify-between items-center">
313
+ <div>
314
+ <div className="font-medium">{t.name}</div>
315
+ <div className="text-xs text-slate-500">{t.columns.length} columns</div>
316
+ </div>
317
+ <div className="flex gap-2">
318
+ <IconButton onClick={() => inspectTable(t.name)}>Preview</IconButton>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ ))}
323
+ </div>
324
+ </CardContent>
325
+ </Card>
326
+ );
327
+ }
328
+
329
+ function WSListener() {
330
+ const { setFileProposal, setShowDiffForFile, selectedFile, pushChat } = useStore();
331
+ const [isWaiting, setIsWaiting] = useState(true);
332
+
333
+ useEffect(() => {
334
+ const socket = io("http://localhost:7860");
335
+
336
+ socket.on("connect", () => {
337
+ console.log("Connected to WebSocket server");
338
+ setIsWaiting(false);
339
+ });
340
+
341
+ socket.on("event", (data) => {
342
+ console.log("WS message:", data);
343
+ if (data.kind === "code_change") {
344
+ setFileProposal(data.filename || selectedFile?.name, {
345
+ code: data.proposedCode,
346
+ isNew: data.isNew,
347
+ suggestedName: data.newFileName,
348
+ });
349
+ setShowDiffForFile(data.filename || selectedFile?.name);
350
+ }
351
+ console.log(data)
352
+ if (data.kind === "agent_message") {
353
+ pushChat({ role: "agent", text: `${data.output}`});
354
+ }
355
+ });
356
+
357
+ socket.on("disconnect", () => {
358
+ console.log("Disconnected from WebSocket server");
359
+ setIsWaiting(true);
360
+ });
361
+
362
+ return () => {
363
+ socket.disconnect();
364
+ };
365
+ }, []);
366
+
367
+ return isWaiting ? <div>Waiting for agent response...</div> : null;
368
+ }
369
+
370
+ function AgentChat() {
371
+ const { chat, pushChat, setFileProposal, setShowDiffForFile, selectedFile, openFile } = useStore();
372
+ const [input, setInput] = useState("");
373
+ const [sending, setSending] = useState(false);
374
+
375
+ async function sendMessage() {
376
+ if (!input.trim() || sending) return;
377
+ pushChat({ role: "user", text: input });
378
+ setInput("");
379
+ setSending(true);
380
+
381
+ try {
382
+ const res = await fetch(`${BASE_URL}/run_crew`, {
383
+ method: "POST",
384
+ headers: { "Content-Type": "application/json" },
385
+ body: JSON.stringify({ instructions: input, file: selectedFile?.name }),
386
+ });
387
+ const data = await res.json();
388
+ pushChat({ role: "agent", text: data.status || "Crew is Working..." });
389
+ } catch (e) {
390
+ pushChat({ role: "agent", text: `Error: ${e.message}` });
391
+ } finally {
392
+ setSending(false);
393
+ }
394
+ }
395
+
396
+ return (
397
+ <Card className="flex flex-col h-full">
398
+ <CardContent className="flex-1 flex flex-col">
399
+ <div className="flex items-center justify-between mb-2">
400
+ <h3 className="text-sm font-semibold">Agent</h3>
401
+ </div>
402
+
403
+ <div className="flex-1 overflow-auto border rounded p-2 bg-slate-50">
404
+ {chat.map((m, i) => (
405
+ <div key={i} className={`mb-2 ${m.role === "user" ? "text-right" : ""}`}>
406
+ <div
407
+ className={`${
408
+ m.role === "user" ? "inline-block bg-blue-600 text-white" : "inline-block bg-white text-slate-800"
409
+ } px-3 py-1 rounded-lg`}
410
+ >
411
+ {m.text}
412
+ {m.proposedCode && (
413
+ <div className="mt-2 p-2 bg-gray-100 rounded">
414
+ <pre className="text-xs overflow-auto">{m.proposedCode}</pre>
415
+ </div>
416
+ )}
417
+ </div>
418
+ </div>
419
+ ))}
420
+ </div>
421
+
422
+ <div className="mt-2 flex gap-2">
423
+ <input
424
+ className="flex-1 border rounded px-3 py-2"
425
+ value={input}
426
+ onChange={(e) => setInput(e.target.value)}
427
+ placeholder="Instruct SQL agent"
428
+ onKeyDown={(e) => {
429
+ if (e.key === "Enter") sendMessage();
430
+ }}
431
+ />
432
+ <IconButton onClick={sendMessage} className="bg-blue-600 text-white">
433
+ {sending ? "..." : "Send"}
434
+ </IconButton>
435
+ </div>
436
+ </CardContent>
437
+ </Card>
438
+ );
439
+ }
440
+
441
+ function OutputConsole() {
442
+ const { consoleOutput } = useStore();
443
+ return (
444
+ <Card>
445
+ <CardContent className="p-0">
446
+ <div className="bg-black text-green-300 font-mono text-sm p-3 h-40 overflow-auto">
447
+ {consoleOutput || "Console output will appear here."}
448
+ </div>
449
+ </CardContent>
450
+ </Card>
451
+ );
452
+ }
453
+
454
+ function SQLMonacoEditor() {
455
+ const {
456
+ editorContent,
457
+ setEditorContent,
458
+ selectedFile,
459
+ showDiffForFile,
460
+ fileProposals,
461
+ fileContents,
462
+ acceptProposal,
463
+ rejectProposal,
464
+ } = useStore();
465
+ const proposal = fileProposals[selectedFile?.name];
466
+ const showDiff = !!proposal?.active && showDiffForFile === selectedFile?.name;
467
+ const proposedContent = proposal?.code || "";
468
+ const isDirty = selectedFile?.name && (!fileContents[selectedFile.name] || editorContent !== fileContents[selectedFile.name]);
469
+
470
+ const saveFile = async () => {
471
+ let fileName = selectedFile?.name;
472
+ if (!fileName) {
473
+ const name = prompt("Save as (must end with .sql):");
474
+ if (!name || !name.endsWith(".sql")) {
475
+ alert("Invalid filename");
476
+ return;
477
+ }
478
+ fileName = name;
479
+ try {
480
+ const res = await fetch(`${BASE_URL}/files`, {
481
+ method: "POST",
482
+ headers: { "Content-Type": "application/json" },
483
+ body: JSON.stringify({ filename: name, content: editorContent }),
484
+ });
485
+ if (!res.ok) throw new Error("Create failed");
486
+ const currentFiles = useStore.getState().files;
487
+ useStore.setState({ files: [...currentFiles, { name }] });
488
+ useStore.setState({ selectedFile: { name }, editorContent, fileContents: { ...useStore.getState().fileContents, [name]: editorContent } });
489
+ alert("File created and saved");
490
+ return;
491
+ } catch (e) {
492
+ alert("Failed to create file: " + e.message);
493
+ return;
494
+ }
495
+ }
496
+ try {
497
+ await fetch(`${BASE_URL}/files/${encodeURIComponent(fileName)}`, {
498
+ method: "PUT",
499
+ headers: { "Content-Type": "application/json" },
500
+ body: JSON.stringify({ content: editorContent }),
501
+ });
502
+ useStore.setState({ fileContents: { ...useStore.getState().fileContents, [fileName]: editorContent } });
503
+ alert("Saved");
504
+ } catch (e) {
505
+ alert("Save failed: " + e.message);
506
+ }
507
+ };
508
+
509
+ const runQuery = async () => {
510
+ let fileName = selectedFile?.name;
511
+ if (!fileName) {
512
+ const name = prompt("Save query as (must end with .sql):");
513
+ if (!name || !name.endsWith(".sql")) {
514
+ alert("Invalid filename");
515
+ return;
516
+ }
517
+ fileName = name;
518
+ try {
519
+ const res = await fetch(`${BASE_URL}/files`, {
520
+ method: "POST",
521
+ headers: { "Content-Type": "application/json" },
522
+ body: JSON.stringify({ filename: name, content: editorContent }),
523
+ });
524
+ if (!res.ok) throw new Error("Create failed");
525
+ const currentFiles = useStore.getState().files;
526
+ useStore.setState({ files: [...currentFiles, { name }], selectedFile: { name }, editorContent, fileContents: { ...useStore.getState().fileContents, [name]: editorContent } });
527
+ } catch (e) {
528
+ alert("Failed to create file: " + e.message);
529
+ return;
530
+ }
531
+ }
532
+ try {
533
+ const res = await fetch(`${BASE_URL}/query`, {
534
+ method: "POST",
535
+ headers: { "Content-Type": "application/json" },
536
+ body: JSON.stringify({ query: editorContent }),
537
+ });
538
+ if (!res.ok) {
539
+ let err;
540
+ try {
541
+ err = await res.json();
542
+ } catch {
543
+ err = "Query failed";
544
+ }
545
+ throw new Error(err.error || JSON.stringify(err) || err);
546
+ }
547
+ const data = await res.json();
548
+ useStore.setState({ consoleOutput: JSON.stringify(data, null, 2) });
549
+ } catch (e) {
550
+ useStore.setState({ consoleOutput: "Run failed: " + e.message });
551
+ }
552
+ };
553
+
554
+ const commonOptions = { minimap: { enabled: false }, fontSize: 13, wordWrap: "on" };
555
+ const editorKey = `${selectedFile?.name}-${showDiff ? "diff" : "normal"}`;
556
+
557
+ return (
558
+ <Card className="h-full flex flex-col">
559
+ <CardContent className="flex-1 flex flex-col p-0">
560
+ <div className="p-3 border-b flex items-center justify-between">
561
+ <div>
562
+ <div className="text-sm font-semibold">{selectedFile?.name || "No file selected"}{isDirty ? " *" : ""}</div>
563
+ <div className="text-xs text-slate-500">SQL Editor {showDiff ? "(Diff View)" : ""}</div>
564
+ </div>
565
+ <div className="flex gap-2">
566
+ {showDiff ? (
567
+ <>
568
+ <IconButton onClick={acceptProposal}>Accept</IconButton>
569
+ <IconButton onClick={rejectProposal} className="bg-red-500">
570
+ Reject
571
+ </IconButton>
572
+ </>
573
+ ) : (
574
+ <>
575
+ <IconButton onClick={saveFile}>Save</IconButton>
576
+ <IconButton onClick={runQuery}>Run</IconButton>
577
+ </>
578
+ )}
579
+ </div>
580
+ </div>
581
+
582
+ <div key={editorKey} className="flex-1">
583
+ {showDiff ? (
584
+ <DiffEditor
585
+ height="100%"
586
+ language="sql"
587
+ original={editorContent}
588
+ modified={proposedContent}
589
+ options={{ ...commonOptions, renderSideBySide: false, diffCodeLens: false }}
590
+ />
591
+ ) : (
592
+ <Editor
593
+ height="100%"
594
+ defaultLanguage="sql"
595
+ value={editorContent}
596
+ onChange={(v) => setEditorContent(v || "")}
597
+ options={commonOptions}
598
+ />
599
+ )}
600
+ </div>
601
+ </CardContent>
602
+ </Card>
603
+ );
604
+ }
605
+
606
+ export default function App() {
607
+ return (
608
+ <div className="h-screen w-screen bg-slate-50 p-4 box-border">
609
+ <div className="grid grid-cols-12 gap-4 h-full">
610
+ <div className="col-span-2 flex flex-col gap-4 h-full">
611
+ <FileExplorer />
612
+ <div className="h-1/3"></div>
613
+ </div>
614
+
615
+ <div className="col-span-7 flex flex-col gap-4 h-full">
616
+ <SQLMonacoEditor />
617
+ </div>
618
+
619
+ <div className="col-span-3 flex flex-col gap-4 h-full">
620
+ <div className="flex-1">
621
+ <AgentChat />
622
+ </div>
623
+ <div className="h-1/3">
624
+ <DBViewer />
625
+ </div>
626
+ <div className="h-1/3">
627
+ <OutputConsole />
628
+ </div>
629
+ </div>
630
+ </div>
631
+ <WSListener />
632
+ </div>
633
+ );
634
+ }
frontend/app/src/assets/react.svg ADDED
frontend/app/src/components/ui/button.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }) {
44
+ const Comp = asChild ? Slot : "button"
45
+
46
+ return (
47
+ <Comp
48
+ data-slot="button"
49
+ className={cn(buttonVariants({ variant, size, className }))}
50
+ {...props} />
51
+ );
52
+ }
53
+
54
+ export { Button, buttonVariants }
frontend/app/src/components/ui/card.jsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({
6
+ className,
7
+ ...props
8
+ }) {
9
+ return (
10
+ <div
11
+ data-slot="card"
12
+ className={cn(
13
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
14
+ className
15
+ )}
16
+ {...props} />
17
+ );
18
+ }
19
+
20
+ function CardHeader({
21
+ className,
22
+ ...props
23
+ }) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
29
+ className
30
+ )}
31
+ {...props} />
32
+ );
33
+ }
34
+
35
+ function CardTitle({
36
+ className,
37
+ ...props
38
+ }) {
39
+ return (
40
+ <div
41
+ data-slot="card-title"
42
+ className={cn("leading-none font-semibold", className)}
43
+ {...props} />
44
+ );
45
+ }
46
+
47
+ function CardDescription({
48
+ className,
49
+ ...props
50
+ }) {
51
+ return (
52
+ <div
53
+ data-slot="card-description"
54
+ className={cn("text-muted-foreground text-sm", className)}
55
+ {...props} />
56
+ );
57
+ }
58
+
59
+ function CardAction({
60
+ className,
61
+ ...props
62
+ }) {
63
+ return (
64
+ <div
65
+ data-slot="card-action"
66
+ className={cn(
67
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
68
+ className
69
+ )}
70
+ {...props} />
71
+ );
72
+ }
73
+
74
+ function CardContent({
75
+ className,
76
+ ...props
77
+ }) {
78
+ return (<div data-slot="card-content" className={cn("px-6", className)} {...props} />);
79
+ }
80
+
81
+ function CardFooter({
82
+ className,
83
+ ...props
84
+ }) {
85
+ return (
86
+ <div
87
+ data-slot="card-footer"
88
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
89
+ {...props} />
90
+ );
91
+ }
92
+
93
+ export {
94
+ Card,
95
+ CardHeader,
96
+ CardFooter,
97
+ CardTitle,
98
+ CardAction,
99
+ CardDescription,
100
+ CardContent,
101
+ }
frontend/app/src/index.css ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ :root {
7
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
8
+ line-height: 1.5;
9
+ font-weight: 400;
10
+
11
+ color-scheme: light dark;
12
+ color: rgba(255, 255, 255, 0.87);
13
+ background-color: #242424;
14
+
15
+ font-synthesis: none;
16
+ text-rendering: optimizeLegibility;
17
+ -webkit-font-smoothing: antialiased;
18
+ -moz-osx-font-smoothing: grayscale;
19
+ --radius: 0.625rem;
20
+ --background: oklch(1 0 0);
21
+ --foreground: oklch(0.145 0 0);
22
+ --card: oklch(1 0 0);
23
+ --card-foreground: oklch(0.145 0 0);
24
+ --popover: oklch(1 0 0);
25
+ --popover-foreground: oklch(0.145 0 0);
26
+ --primary: oklch(0.205 0 0);
27
+ --primary-foreground: oklch(0.985 0 0);
28
+ --secondary: oklch(0.97 0 0);
29
+ --secondary-foreground: oklch(0.205 0 0);
30
+ --muted: oklch(0.97 0 0);
31
+ --muted-foreground: oklch(0.556 0 0);
32
+ --accent: oklch(0.97 0 0);
33
+ --accent-foreground: oklch(0.205 0 0);
34
+ --destructive: oklch(0.577 0.245 27.325);
35
+ --border: oklch(0.922 0 0);
36
+ --input: oklch(0.922 0 0);
37
+ --ring: oklch(0.708 0 0);
38
+ --chart-1: oklch(0.646 0.222 41.116);
39
+ --chart-2: oklch(0.6 0.118 184.704);
40
+ --chart-3: oklch(0.398 0.07 227.392);
41
+ --chart-4: oklch(0.828 0.189 84.429);
42
+ --chart-5: oklch(0.769 0.188 70.08);
43
+ --sidebar: oklch(0.985 0 0);
44
+ --sidebar-foreground: oklch(0.145 0 0);
45
+ --sidebar-primary: oklch(0.205 0 0);
46
+ --sidebar-primary-foreground: oklch(0.985 0 0);
47
+ --sidebar-accent: oklch(0.97 0 0);
48
+ --sidebar-accent-foreground: oklch(0.205 0 0);
49
+ --sidebar-border: oklch(0.922 0 0);
50
+ --sidebar-ring: oklch(0.708 0 0);
51
+ }
52
+
53
+ a {
54
+ font-weight: 500;
55
+ color: #646cff;
56
+ text-decoration: inherit;
57
+ }
58
+ a:hover {
59
+ color: #535bf2;
60
+ }
61
+
62
+ body {
63
+ margin: 0;
64
+ display: flex;
65
+ place-items: center;
66
+ min-width: 320px;
67
+ min-height: 100vh;
68
+ }
69
+
70
+ h1 {
71
+ font-size: 3.2em;
72
+ line-height: 1.1;
73
+ }
74
+
75
+ button {
76
+ border-radius: 8px;
77
+ border: 1px solid transparent;
78
+ padding: 0.6em 1.2em;
79
+ font-size: 1em;
80
+ font-weight: 500;
81
+ font-family: inherit;
82
+ background-color: #1a1a1a;
83
+ cursor: pointer;
84
+ transition: border-color 0.25s;
85
+ }
86
+ button:hover {
87
+ border-color: #646cff;
88
+ }
89
+ button:focus,
90
+ button:focus-visible {
91
+ outline: 4px auto -webkit-focus-ring-color;
92
+ }
93
+
94
+ @media (prefers-color-scheme: light) {
95
+ :root {
96
+ color: #213547;
97
+ background-color: #ffffff;
98
+ }
99
+ a:hover {
100
+ color: #747bff;
101
+ }
102
+ button {
103
+ background-color: #f9f9f9;
104
+ }
105
+ }
106
+
107
+ @theme inline {
108
+ --radius-sm: calc(var(--radius) - 4px);
109
+ --radius-md: calc(var(--radius) - 2px);
110
+ --radius-lg: var(--radius);
111
+ --radius-xl: calc(var(--radius) + 4px);
112
+ --color-background: var(--background);
113
+ --color-foreground: var(--foreground);
114
+ --color-card: var(--card);
115
+ --color-card-foreground: var(--card-foreground);
116
+ --color-popover: var(--popover);
117
+ --color-popover-foreground: var(--popover-foreground);
118
+ --color-primary: var(--primary);
119
+ --color-primary-foreground: var(--primary-foreground);
120
+ --color-secondary: var(--secondary);
121
+ --color-secondary-foreground: var(--secondary-foreground);
122
+ --color-muted: var(--muted);
123
+ --color-muted-foreground: var(--muted-foreground);
124
+ --color-accent: var(--accent);
125
+ --color-accent-foreground: var(--accent-foreground);
126
+ --color-destructive: var(--destructive);
127
+ --color-border: var(--border);
128
+ --color-input: var(--input);
129
+ --color-ring: var(--ring);
130
+ --color-chart-1: var(--chart-1);
131
+ --color-chart-2: var(--chart-2);
132
+ --color-chart-3: var(--chart-3);
133
+ --color-chart-4: var(--chart-4);
134
+ --color-chart-5: var(--chart-5);
135
+ --color-sidebar: var(--sidebar);
136
+ --color-sidebar-foreground: var(--sidebar-foreground);
137
+ --color-sidebar-primary: var(--sidebar-primary);
138
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
139
+ --color-sidebar-accent: var(--sidebar-accent);
140
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
141
+ --color-sidebar-border: var(--sidebar-border);
142
+ --color-sidebar-ring: var(--sidebar-ring);
143
+ }
144
+
145
+ .dark {
146
+ --background: oklch(0.145 0 0);
147
+ --foreground: oklch(0.985 0 0);
148
+ --card: oklch(0.205 0 0);
149
+ --card-foreground: oklch(0.985 0 0);
150
+ --popover: oklch(0.205 0 0);
151
+ --popover-foreground: oklch(0.985 0 0);
152
+ --primary: oklch(0.922 0 0);
153
+ --primary-foreground: oklch(0.205 0 0);
154
+ --secondary: oklch(0.269 0 0);
155
+ --secondary-foreground: oklch(0.985 0 0);
156
+ --muted: oklch(0.269 0 0);
157
+ --muted-foreground: oklch(0.708 0 0);
158
+ --accent: oklch(0.269 0 0);
159
+ --accent-foreground: oklch(0.985 0 0);
160
+ --destructive: oklch(0.704 0.191 22.216);
161
+ --border: oklch(1 0 0 / 10%);
162
+ --input: oklch(1 0 0 / 15%);
163
+ --ring: oklch(0.556 0 0);
164
+ --chart-1: oklch(0.488 0.243 264.376);
165
+ --chart-2: oklch(0.696 0.17 162.48);
166
+ --chart-3: oklch(0.769 0.188 70.08);
167
+ --chart-4: oklch(0.627 0.265 303.9);
168
+ --chart-5: oklch(0.645 0.246 16.439);
169
+ --sidebar: oklch(0.205 0 0);
170
+ --sidebar-foreground: oklch(0.985 0 0);
171
+ --sidebar-primary: oklch(0.488 0.243 264.376);
172
+ --sidebar-primary-foreground: oklch(0.985 0 0);
173
+ --sidebar-accent: oklch(0.269 0 0);
174
+ --sidebar-accent-foreground: oklch(0.985 0 0);
175
+ --sidebar-border: oklch(1 0 0 / 10%);
176
+ --sidebar-ring: oklch(0.556 0 0);
177
+ }
178
+
179
+ @layer base {
180
+ * {
181
+ @apply border-border outline-ring/50;
182
+ }
183
+ body {
184
+ @apply bg-background text-foreground;
185
+ }
186
+ }
frontend/app/src/lib/utils.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs));
6
+ }
frontend/app/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/app/vite.config.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "path"
2
+ import tailwindcss from "@tailwindcss/vite"
3
+ import react from "@vitejs/plugin-react"
4
+ import { defineConfig } from "vite"
5
+
6
+ // https://vite.dev/config/
7
+ export default defineConfig({
8
+ plugins: [react(), tailwindcss()],
9
+ resolve: {
10
+ alias: {
11
+ "@": path.resolve(__dirname, "./src"),
12
+ },
13
+ },
14
+ })
poem.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ CrewAI is the ace of poetic pace, crafting verse with AI in its perfect place.
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
sql_files/app.db ADDED
Binary file (24.6 kB). View file
 
sql_files/schema.sql ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE TABLE Fruits (
2
+ fruit_id INT PRIMARY KEY,
3
+ name VARCHAR(255),
4
+ type VARCHAR(100)
5
+ );
6
+
7
+ CREATE TABLE Tastes (
8
+ taste_id INT PRIMARY KEY,
9
+ description VARCHAR(255)
10
+ );
11
+
12
+ CREATE TABLE Fruit_Tastes (
13
+ fruit_id INT NOT NULL,
14
+ taste_id INT NOT NULL,
15
+ CONSTRAINT fk_fruit FOREIGN KEY (fruit_id) REFERENCES Fruits(fruit_id),
16
+ CONSTRAINT fk_taste FOREIGN KEY (taste_id) REFERENCES Tastes(taste_id)
17
+ );
src/__pycache__/app.cpython-313.pyc ADDED
Binary file (11.8 kB). View file
 
src/app.py CHANGED
@@ -1,11 +1,205 @@
1
- from flask import Flask
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- app = Flask(__name__)
4
 
5
- @app.route("/")
6
- def home():
7
- return "Flask + Ollama on Hugging Face Spaces!"
8
 
9
- @app.route("/health")
10
- def health():
11
- return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory, render_template
2
+ import os
3
+ import sqlite3
4
+ import subprocess
5
+ import threading
6
+ import queue
7
+ from flask import Response
8
+ from flask_cors import CORS
9
+ import json
10
+ import re
11
+ from datetime import datetime
12
+ from flask_socketio import SocketIO, emit
13
 
 
14
 
15
+ event_queue = queue.Queue()
 
 
16
 
17
+ app = Flask(__name__, static_folder='../frontend/app/dist', static_url_path='/')
18
+ socketio = SocketIO(app, cors_allowed_origins="*")
19
+
20
+ __all__ = ["app", "socketio"]
21
+
22
+
23
+ from codeagent.src.codeagent.main import CodeAgentFlow
24
+
25
+ CORS(app)
26
+
27
+ SQL_FILES_DIR = 'sql_files'
28
+ os.makedirs(SQL_FILES_DIR, exist_ok=True)
29
+ DB_FILE = 'app.db'
30
+
31
+ @app.route('/')
32
+ def index():
33
+ return send_from_directory(app.static_folder, 'index.html')
34
+
35
+ @app.route('/files', methods=['GET'])
36
+ def get_files():
37
+ files = os.listdir(SQL_FILES_DIR)
38
+ return jsonify([{'name': f} for f in files])
39
+
40
+ @app.route('/files', methods=['POST'])
41
+ def create_file():
42
+ data = request.json
43
+ filename = data['filename']
44
+ content = data.get('content', '')
45
+ filepath = os.path.join(SQL_FILES_DIR, filename)
46
+ with open(filepath, 'w') as f:
47
+ f.write(content)
48
+ return jsonify({"status": "created", "file": filename})
49
+
50
+ @app.route('/files/<filename>', methods=['PUT'])
51
+ def update_file(filename):
52
+ content = request.json.get('content', '')
53
+ filepath = os.path.join(SQL_FILES_DIR, filename)
54
+ if not os.path.exists(filepath):
55
+ return jsonify({"error": "file not found"}), 404
56
+ with open(filepath, 'w') as f:
57
+ f.write(content)
58
+ return jsonify({"status": "updated", "file": filename})
59
+
60
+ @app.route('/files/<filename>', methods=['GET'])
61
+ def get_file_content(filename):
62
+ filepath = os.path.join(SQL_FILES_DIR, filename)
63
+ if os.path.exists(filepath):
64
+ with open(filepath, 'r') as f:
65
+ content = f.read()
66
+ return jsonify({"filename": filename, "content": content})
67
+ return jsonify({"error": "file not found"}), 404
68
+
69
+ @app.route('/files/<filename>', methods=['DELETE'])
70
+ def delete_file(filename):
71
+ filepath = os.path.join(SQL_FILES_DIR, filename)
72
+ if os.path.exists(filepath):
73
+ os.remove(filepath)
74
+ return jsonify({"status": "deleted", "file": filename})
75
+ return jsonify({"error": "file not found"}), 404
76
+
77
+
78
+ def get_db_connection():
79
+ conn = sqlite3.connect(os.path.join(SQL_FILES_DIR,DB_FILE))
80
+ conn.row_factory = sqlite3.Row
81
+ return conn
82
+
83
+ @app.route('/tables', methods=['GET'])
84
+ def get_tables():
85
+ conn = get_db_connection()
86
+ cursor = conn.cursor()
87
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
88
+ tables = [row['name'] for row in cursor.fetchall()]
89
+ conn.close()
90
+ return jsonify(tables)
91
+
92
+ recent_result = None
93
+ @app.route('/query', methods=['POST'])
94
+ def run_query():
95
+ query = request.json.get('query', '')
96
+ print(query)
97
+ conn = get_db_connection()
98
+ cursor = conn.cursor()
99
+ try:
100
+ query_lc = str(query).strip().lower()
101
+
102
+ if ";" in query.strip() and not query_lc.startswith("select") and not query_lc.startswith("pragma"):
103
+ # Multiple statements → executescript
104
+ cursor.executescript(query)
105
+ conn.commit()
106
+ result = {"rows_affected": cursor.rowcount}
107
+ else:
108
+ cursor.execute(query)
109
+
110
+ if query_lc.startswith("pragma"):
111
+ rows = cursor.fetchall()
112
+ result = [dict(row) for row in rows] if rows else []
113
+ elif query_lc.startswith("select"):
114
+ result = [dict(row) for row in cursor.fetchall()]
115
+ else:
116
+ conn.commit()
117
+ result = {"rows_affected": cursor.rowcount}
118
+
119
+ conn.close()
120
+ global recent_result
121
+ recent_result = {"query": query, "result": result}
122
+
123
+ return jsonify(result)
124
+ except Exception as e:
125
+ conn.close()
126
+ return jsonify({"error": str(e)}), 400
127
+
128
+ @app.route('/recent_query', methods=['GET'])
129
+ def recent_query():
130
+ return jsonify(recent_result)
131
+
132
+ @app.route('/execute_file', methods=['POST'])
133
+ def execute_file():
134
+ """
135
+ Execute a .sql file located in SQL_FILES_DIR using sqlite3.executescript.
136
+ Accepts: { "filename": "schema.sql" }
137
+ Returns: { "status": "success", "file": "...", "started_at": "...", "finished_at": "..."}
138
+ or { "error": "message" }
139
+ """
140
+ data = request.json or {}
141
+ filename = data.get('filename')
142
+ if not filename:
143
+ return jsonify({"error": "filename is required"}), 400
144
+
145
+ filepath = os.path.join(SQL_FILES_DIR, filename)
146
+ if not os.path.exists(filepath):
147
+ return jsonify({"error": f"file not found: {filename}"}), 404
148
+
149
+ started_at = datetime.utcnow().isoformat()
150
+ conn = get_db_connection()
151
+ try:
152
+ with open(filepath, 'r', encoding='utf-8') as f:
153
+ sql_text = f.read()
154
+
155
+ conn.executescript(sql_text)
156
+ conn.commit()
157
+ finished_at = datetime.utcnow().isoformat()
158
+ return jsonify({
159
+ "status": "success",
160
+ "file": filename,
161
+ "started_at": started_at,
162
+ "finished_at": finished_at
163
+ })
164
+ except Exception as e:
165
+ conn.rollback()
166
+ return jsonify({"error": str(e), "file": filename}), 400
167
+ finally:
168
+ conn.close()
169
+
170
+ def run_crew_task(user_input):
171
+ codeAgent = CodeAgentFlow(user_input=user_input)
172
+ codeAgent.kickoff()
173
+
174
+ @app.route('/run_crew', methods=['POST'])
175
+ def run_crew():
176
+ instructions = request.json.get('instructions', '')
177
+ threading.Thread(target=run_crew_task, args=(instructions,)).start()
178
+ return jsonify({"status": "Crew is Working..."})
179
+
180
+ def send_event(msg):
181
+ socketio.emit("event", msg)
182
+
183
+ @app.route('/send')
184
+ def sendSSE():
185
+ msg = {
186
+ "filename": "Users_query.sql",
187
+ "proposedCode": "SELECT * FROM Users;",
188
+ "isNew": False,
189
+ "newFileName": "Users_query.sql",
190
+ "kind": "code_change"
191
+ }
192
+ send_event(msg)
193
+ return "sent"
194
+
195
+ @socketio.on('connect')
196
+ def handle_connect():
197
+ print("Client connected")
198
+ emit("event", {"status": "connected"})
199
+
200
+ @socketio.on('disconnect')
201
+ def handle_disconnect():
202
+ print("Client disconnected")
203
+
204
+ if __name__ == '__main__':
205
+ socketio.run(app, host="0.0.0.0", port=7860, debug=False)
src/codeagent/.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ lib/
4
+ .DS_Store
src/codeagent/README.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # {{crew_name}} Crew
2
+
3
+ Welcome to the {{crew_name}} Crew project, powered by [crewAI](https://crewai.com). This template is designed to help you set up a multi-agent AI system with ease, leveraging the powerful and flexible framework provided by crewAI. Our goal is to enable your agents to collaborate effectively on complex tasks, maximizing their collective intelligence and capabilities.
4
+
5
+ ## Installation
6
+
7
+ Ensure you have Python >=3.10 <3.14 installed on your system. This project uses [UV](https://docs.astral.sh/uv/) for dependency management and package handling, offering a seamless setup and execution experience.
8
+
9
+ First, if you haven't already, install uv:
10
+
11
+ ```bash
12
+ pip install uv
13
+ ```
14
+
15
+ Next, navigate to your project directory and install the dependencies:
16
+
17
+ (Optional) Lock the dependencies and install them by using the CLI command:
18
+ ```bash
19
+ crewai install
20
+ ```
21
+
22
+ ### Customizing
23
+
24
+ **Add your `OPENAI_API_KEY` into the `.env` file**
25
+
26
+ - Modify `src/codeagent/config/agents.yaml` to define your agents
27
+ - Modify `src/codeagent/config/tasks.yaml` to define your tasks
28
+ - Modify `src/codeagent/crew.py` to add your own logic, tools and specific args
29
+ - Modify `src/codeagent/main.py` to add custom inputs for your agents and tasks
30
+
31
+ ## Running the Project
32
+
33
+ To kickstart your flow and begin execution, run this from the root folder of your project:
34
+
35
+ ```bash
36
+ crewai run
37
+ ```
38
+
39
+ This command initializes the codeagent Flow as defined in your configuration.
40
+
41
+ This example, unmodified, will run the create a `report.md` file with the output of a research on LLMs in the root folder.
42
+
43
+ ## Understanding Your Crew
44
+
45
+ The codeagent Crew is composed of multiple AI agents, each with unique roles, goals, and tools. These agents collaborate on a series of tasks, defined in `config/tasks.yaml`, leveraging their collective skills to achieve complex objectives. The `config/agents.yaml` file outlines the capabilities and configurations of each agent in your crew.
46
+
47
+ ## Support
48
+
49
+ For support, questions, or feedback regarding the {{crew_name}} Crew or crewAI.
50
+
51
+ - Visit our [documentation](https://docs.crewai.com)
52
+ - Reach out to us through our [GitHub repository](https://github.com/joaomdmoura/crewai)
53
+ - [Join our Discord](https://discord.com/invite/X4JWnZnxPb)
54
+ - [Chat with our docs](https://chatg.pt/DWjSBZn)
55
+
56
+ Let's create wonders together with the power and simplicity of crewAI.
src/codeagent/pyproject.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "codeagent"
3
+ version = "0.1.0"
4
+ description = "codeagent using crewAI"
5
+ authors = [{ name = "Your Name", email = "you@example.com" }]
6
+ requires-python = ">=3.10,<3.14"
7
+ dependencies = [
8
+ "crewai[tools]>=0.177.0,<1.0.0",
9
+ ]
10
+
11
+ [project.scripts]
12
+ kickoff = "codeagent.main:kickoff"
13
+ run_crew = "codeagent.main:kickoff"
14
+ plot = "codeagent.main:plot"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.crewai]
21
+ type = "flow"
src/codeagent/src/codeagent/__init__.py ADDED
File without changes
src/codeagent/src/codeagent/crews/SqlCrew/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Poem crew template."""
src/codeagent/src/codeagent/crews/SqlCrew/config/agents.yaml ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ manager:
2
+ role: "Crew Manager"
3
+ goal: >
4
+ Understand the user's intent and choose the minimal set of tasks and the best-suited agents
5
+ to fulfill it safely. Avoid destructive ops unless explicitly requested.
6
+ If user only needs a SELECT, do not run DDL or DML. Do not allow any agent except
7
+ the SQL Writing Agent to author SQL queries or write .sql files.
8
+ backstory: >
9
+ A seasoned release lead who routes work precisely.
10
+ Always follows the principle of least privilege and avoids unnecessary steps.
11
+ allow_delegation: true
12
+ verbose: true
13
+
14
+ schema_agent:
15
+ role: "Schema Agent"
16
+ goal: >
17
+ Design robust, normalized database schemas and validate correctness
18
+ (tables, columns, types, primary keys, foreign keys).
19
+ Do NOT write SQL queries or save .sql files.
20
+ backstory: >
21
+ A meticulous database engineer focused on relational integrity,
22
+ evolution-friendly design, and practical constraints.
23
+ verbose: true
24
+
25
+ sql_exec_agent:
26
+ role: "SQL Execution Agent"
27
+ goal: >
28
+ Execute .sql files in the correct order (setup → schema → data),
29
+ and report results clearly with errors if any.
30
+ backstory: >
31
+ A release engineer for database changes, automating the sequence of DDL and DML executions.
32
+ verbose: true
33
+
34
+ code_quality_agent:
35
+ role: "Code Quality Agent"
36
+ goal: >
37
+ Review SQL for best practices, detect anti-patterns, and suggest improvements.
38
+ Do NOT author new SQL unless explicitly asked to propose a corrected snippet.
39
+ Never write files.
40
+ backstory: >
41
+ A vigilant reviewer who enforces consistency, performance awareness,
42
+ and safe data modification patterns.
43
+ verbose: true
44
+
45
+ sql_writing_agent:
46
+ role: "SQL Writing Agent"
47
+ goal: >
48
+ Generate safe, performant SQL queries based on the user's intent and the schema context,
49
+ lint them for common issues, and propose the final SQL via propose_code_changes tool for UI review and save.
50
+ Only produce SELECT/READ queries unless the user explicitly requests DML/DDL.
51
+ Avoid SELECT *; prefer explicit columns; document filters at the top of the file.
52
+ backstory: >
53
+ An analyst who writes clear, maintainable SQL. Prioritizes readability, safety,
54
+ and reproducibility. Collaborates with Schema Agent for schema awareness
55
+ but alone is authorized to write .sql files.
56
+ verbose: true
src/codeagent/src/codeagent/crews/SqlCrew/config/tasks.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ tasks:
2
+ analyze_intent:
3
+ description: >
4
+ Analyze the user's request to determine required actions: schema design, query writing, review, or execution.
5
+ Delegate to appropriate agents only if necessary. Prioritize safety and minimal steps.
6
+ expected_output: A clear plan of delegated tasks or direct response if no delegation needed.
7
+ agent: manager
8
+
9
+ design_schema:
10
+ description: >
11
+ If schema changes or validation needed, design normalized schema with tables, columns, types, PKs, FKs.
12
+ expected_output: Detailed schema proposal in text format, ready for SQL generation.
13
+ agent: schema_agent
14
+
15
+ write_sql:
16
+ description: >
17
+ Based on user intent and schema, generate SQL query or script. Lint for issues. Save to .sql file via tool.
18
+ Default to SELECT unless DML/DDL explicitly requested.
19
+ expected_output: Generated SQL code saved to file, with lint report.
20
+ agent: sql_writing_agent
21
+
22
+ review_sql:
23
+ description: >
24
+ Review generated or existing SQL for best practices, anti-patterns, performance, safety.
25
+ Suggest improvements without writing new code unless requested.
26
+ expected_output: Review report with issues and suggestions.
27
+ agent: code_quality_agent
28
+
29
+ execute_sql:
30
+ description: >
31
+ If execution requested, run .sql files in order (schema first, then data). Report results and errors.
32
+ expected_output: Execution log with success/failure and output.
33
+ agent: sql_exec_agent
src/codeagent/src/codeagent/crews/SqlCrew/sqlcrew.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from crewai import Agent, Crew, Process, Task, LLM
3
+ from crewai.project import CrewBase, agent, crew, task
4
+ from crewai.agents.agent_builder.base_agent import BaseAgent
5
+ import sys
6
+ import os
7
+
8
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")))
9
+ from app import socketio
10
+
11
+ from codeagent.src.codeagent.tools.custom_tools import execute_sql_file, get_files_and_schema, propose_code_changes, get_recent_query_result
12
+
13
+ llm = LLM(model="ollama/llama3.1:8b-instruct-q4_0", base_url="http://localhost:11434")
14
+
15
+
16
+ def send_chat_message(output: str) -> None:
17
+ socketio.emit("event", {'kind':'agent_message', 'output': output})
18
+
19
+ @CrewBase
20
+ class SqlCrew:
21
+ agents: List[BaseAgent]
22
+ tasks: List[Task]
23
+
24
+ # def manager(self) -> Agent:
25
+ # return Agent(
26
+ # role="Crew Manager",
27
+ # goal=(
28
+ # "Understand user instruction and delegate tasks according to the instruction, \"{task}\" "
29
+ # "Schema-related → Schema Agent, SQL writing → SQL Writing Agent, "
30
+ # "execution → SQL Execution Agent, reviews/errors → Code Quality Agent."
31
+ # ),
32
+ # backstory="A release manager who ensures the right agent handles the right task.",
33
+ # llm=llm,
34
+ # allow_delegation=True,
35
+ # verbose=True,
36
+ # )
37
+
38
+ @agent
39
+ def schema_agent(self) -> Agent:
40
+ return Agent(
41
+ role="Schema Agent",
42
+ goal=(
43
+ "Check if you have anything to do with the task else delegate to another Co-Worker, Task: {task}"
44
+ "Design or validate database schema, if it is asked by user"
45
+ "Focus on tables, columns, keys, and relations. "
46
+ "Do NOT write SQL files; just propose schema."
47
+ ),
48
+ backstory="A careful database engineer ensuring solid relational design.",
49
+ llm=llm,
50
+ tools=[get_files_and_schema],
51
+ allow_delegation=True,
52
+ step_callback = send_chat_message,
53
+ verbose=True
54
+ )
55
+
56
+ @agent
57
+ def sql_writing_agent(self) -> Agent:
58
+ return Agent(
59
+ role="SQL Writing Agent",
60
+ goal=(
61
+ "Check if you have anything to do with the task else delegate to another Co-Worker, Task: {task}"
62
+ "Write safe, performant SQL queries. "
63
+ "Propose code changes for UI approval instead of direct execution."
64
+ ),
65
+ backstory="An SQL analyst who produces clean, maintainable queries.",
66
+ llm=llm,
67
+ tools=[propose_code_changes, get_files_and_schema],
68
+ allow_delegation=True,
69
+ step_callback = send_chat_message,
70
+ verbose=True,
71
+ )
72
+
73
+ @agent
74
+ def code_quality_agent(self) -> Agent:
75
+ return Agent(
76
+ role="Code Quality Agent",
77
+ goal=(
78
+ "Check if you have anything to do with the task else delegate to another Co-Worker, Task: {task}"
79
+ "Review SQL for performance, safety, and best practices. "
80
+ "Suggest improvements but don’t auto-correct unless asked."
81
+ ),
82
+ backstory="A strict reviewer ensuring SQL is clean and safe.",
83
+ llm=llm,
84
+ tools=[get_recent_query_result, get_files_and_schema],
85
+ allow_delegation=True,
86
+ step_callback = send_chat_message,
87
+ verbose=True,
88
+ )
89
+
90
+ @agent
91
+ def sql_exec_agent(self) -> Agent:
92
+ return Agent(
93
+ role="SQL Execution Agent",
94
+ goal=(
95
+ "Check if you have anything to do with the task else delegate to another Co-Worker, Task: {task}"
96
+ "Execute .sql files when asked. "
97
+ "If execution fails → send to Code Quality Agent. "
98
+ "If successful → report back to Manager."
99
+ ),
100
+ backstory="A release engineer who safely runs DB updates.",
101
+ llm=llm,
102
+ tools=[execute_sql_file, get_files_and_schema],
103
+ allow_delegation=True,
104
+ step_callback = send_chat_message,
105
+ verbose=True,
106
+ )
107
+
108
+ # @task
109
+ # def analyze_intent(self) -> Task:
110
+ # return Task(
111
+ # description="Analyze user request and route to the right agent.",
112
+ # expected_output="Clear delegation plan or direct response.",
113
+ # agent=self.manager(),
114
+ # )
115
+
116
+ @task
117
+ def design_schema(self) -> Task:
118
+ return Task(
119
+ description="Propose schema (tables, columns, PKs, FKs) if needed meaning if user specifically said so to create database or to change database.",
120
+ expected_output="Schema design in text format.",
121
+ agent=self.schema_agent(),
122
+ )
123
+
124
+ @task
125
+ def write_sql(self) -> Task:
126
+ return Task(
127
+ description="Generate SQL based on schema and intent. Save via propose_code_changes.",
128
+ expected_output="SQL file proposal + lint notes.",
129
+ agent=self.sql_writing_agent(),
130
+ )
131
+
132
+
133
+ @task
134
+ def execute_sql(self) -> Task:
135
+ return Task(
136
+ description="Run SQL files if execution requested. Report results/errors.",
137
+ expected_output="Execution log with success/failure details.",
138
+ agent=self.sql_exec_agent(),
139
+ )
140
+
141
+ @task
142
+ def review_sql(self) -> Task:
143
+ return Task(
144
+ description="Check SQL for issues and suggest improvements.",
145
+ expected_output="Review report with issues & recommendations.",
146
+ agent=self.code_quality_agent(),
147
+ )
148
+
149
+ @crew
150
+ def crew(self) -> Crew:
151
+ return Crew(
152
+ agents=self.agents,
153
+ tasks=self.tasks,
154
+ process=Process.hierarchical,
155
+ manager_llm = llm,
156
+ verbose=True,
157
+ step_callback=send_chat_message,
158
+ task_callback=send_chat_message,
159
+
160
+ )
src/codeagent/src/codeagent/main.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from pydantic import BaseModel, Field
3
+
4
+ from crewai.flow import Flow, listen, start
5
+ from crewai import Agent, LLM
6
+
7
+ from typing import Literal
8
+
9
+
10
+ # from codeagent.src.codeagent.crews.git_crew.git_crew import GitCrew
11
+ from codeagent.src.codeagent.crews.SqlCrew.sqlcrew import SqlCrew
12
+
13
+ class FlowState(BaseModel):
14
+ user_input: str = ""
15
+ decision: str = ""
16
+ result: str = ""
17
+
18
+ class Decision(BaseModel):
19
+ decision:str = Field(..., description='identifer of the Crew to Call to process input further, ["GitCrew","SqlCrew"]')
20
+
21
+ class DebugLLM(LLM):
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+
25
+ def call(self, messages, *args, **kwargs):
26
+ import json
27
+ print("=== LLM Payload Sent ===")
28
+ print(json.dumps(messages, indent=2))
29
+ print("========================")
30
+ return super().call(messages, *args, **kwargs)
31
+
32
+ llm = DebugLLM(model="ollama/llama3.1:8b-instruct-q4_0", base_url="http://localhost:11434")
33
+
34
+ router_agent = Agent(
35
+ role="Task Router",
36
+ goal = 'Decide which crew (GitCrew or SqlCrew) should handle the user request. ' \
37
+ 'Return the decision as a **JSON object** with one field "decision", ' \
38
+ 'and make sure the value is a string in double quotes, like: {"decision": "GitCrew"} or {"decision": "SqlCrew"}',
39
+ backstory= "An intelligent orchestrator that analyzes user input and delegates tasks to specialized crews. You have two crews One is GitCrew and Other one is SqlCrew" \
40
+ "GitCrew description: Handles tasks related to git, repositories, commits, branches, merges, etc." \
41
+ "SqlCrew description: Handles tasks related to databases, SQL queries, tables, schemas, etc.",
42
+ llm=llm,
43
+ verbose=True,
44
+ )
45
+
46
+ class CodeAgentFlow(Flow[FlowState]):
47
+
48
+ @start()
49
+ def decide_crew(self):
50
+ print("State ------:> ", self.state.user_input)
51
+ decision_obj = router_agent.kickoff(self.state.user_input, response_format=Decision)
52
+ print(decision_obj)
53
+ decision_obj = Decision.model_validate_json(decision_obj.raw)
54
+ self.state.decision = decision_obj.decision
55
+ if self.state.decision == "GitCrew":
56
+ # result = GitCrew().crew().kickoff(inputs={"task": self.state.user_input})
57
+ # self.state.result = result.raw
58
+ pass
59
+ elif self.state.decision == "SqlCrew":
60
+ result = SqlCrew().crew().kickoff(inputs={"task": self.state.user_input})
61
+ self.state.result = result.raw
62
+ else:
63
+ self.state.result = f"Invalid decision from router: {self.state.decision}"
64
+
65
+
66
+ @listen(decide_crew)
67
+ def save_result(self):
68
+ print("Saving result...")
69
+ with open("crew_result.txt", "w") as f:
70
+ f.write(self.state.result)
71
+
72
+
73
+ def kickoff(user_input: str):
74
+ code_agent_flow = CodeAgentFlow()
75
+ code_agent_flow.state.user_input = user_input
76
+ code_agent_flow.kickoff()
77
+
78
+
79
+ def plot():
80
+ code_agent_flow = CodeAgentFlow()
81
+ code_agent_flow.plot()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ # Example kickoff
86
+ kickoff("please push my changes to git repository")
src/codeagent/src/codeagent/tools/__init__.py ADDED
File without changes
src/codeagent/src/codeagent/tools/custom_tools.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ from crewai.tools import tool
4
+ import queue
5
+ import sys
6
+ import os
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")))
8
+ from app import socketio
9
+
10
+ @tool("execute_sql_file")
11
+ def execute_sql_file(filename: str) -> str:
12
+ """Executes the provided .sql file via Flask and returns the result."""
13
+ resp = requests.post("http://localhost:7860/execute_file", json={"filename": filename})
14
+ if resp.status_code == 200:
15
+ return json.dumps(resp.json())
16
+ return f"Error: {resp.text}"
17
+
18
+ @tool("get_files_and_schema")
19
+ def get_files_and_schema() -> str:
20
+ """Retrieves all .sql files with contents and full DB schema (tables, columns, types) via Flask."""
21
+ files_resp = requests.get("http://localhost:7860/files")
22
+ sql_files = []
23
+ if files_resp.ok:
24
+ file_list = [f['name'] for f in files_resp.json() if f['name'].endswith('.sql')]
25
+ for fname in file_list:
26
+ content_resp = requests.get(f"http://localhost:7860/files/{fname}")
27
+ if content_resp.ok:
28
+ content = content_resp.json().get('content', '')
29
+ sql_files.append(f"File: {fname}\n{content}\n{'-'*50}")
30
+
31
+ schema = []
32
+ tables_resp = requests.get("http://localhost:7860/tables")
33
+ if tables_resp.ok:
34
+ tables = tables_resp.json()
35
+ for t in tables:
36
+ qresp = requests.post("http://localhost:7860/query", json={"query": f"PRAGMA table_info({t})"})
37
+ if qresp.ok:
38
+ cols = qresp.json()
39
+ col_str = '\n'.join([f"{c['name']} {c.get('type', 'UNKNOWN')}" for c in cols])
40
+ schema.append(f"Table: {t}\n{col_str}\n{'-'*50}")
41
+
42
+ return f"SQL Files:\n{chr(10).join(sql_files)}\n\nDB Schema:\n{chr(10).join(schema) or "Empty"}"
43
+
44
+
45
+ @tool("propose_code_changes")
46
+ def propose_code_changes(filename: str, code: str, is_new: bool = False, suggested_name: str = None) -> str:
47
+ """Proposes code changes or new file for frontend UI via WebSocket."""
48
+ proposal = {
49
+ "kind": "code_change",
50
+ "proposedCode": code,
51
+ "newFileName": suggested_name if is_new else None,
52
+ "isNew": is_new,
53
+ "filename": filename
54
+ }
55
+ socketio.emit("event", proposal)
56
+ return "Proposal sent to frontend via WebSocket"
57
+
58
+ @tool("get_recent_query_result")
59
+ def get_recent_query_result() -> str:
60
+ """Retrieves the most recent SQL query execution result via Flask."""
61
+ resp = requests.get("http://localhost:7860/recent_query")
62
+ if resp.status_code == 200:
63
+ return resp.json().get('result', 'No recent result')
64
+ return "Error fetching recent result"