fastapi-fullstack 0.1.7__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/METADATA +9 -2
  2. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/RECORD +71 -55
  3. fastapi_gen/__init__.py +6 -1
  4. fastapi_gen/cli.py +9 -0
  5. fastapi_gen/config.py +154 -2
  6. fastapi_gen/generator.py +34 -14
  7. fastapi_gen/prompts.py +172 -31
  8. fastapi_gen/template/VARIABLES.md +33 -4
  9. fastapi_gen/template/cookiecutter.json +10 -0
  10. fastapi_gen/template/hooks/post_gen_project.py +87 -2
  11. fastapi_gen/template/{{cookiecutter.project_slug}}/.env.prod.example +9 -0
  12. fastapi_gen/template/{{cookiecutter.project_slug}}/.gitlab-ci.yml +178 -0
  13. fastapi_gen/template/{{cookiecutter.project_slug}}/CLAUDE.md +3 -0
  14. fastapi_gen/template/{{cookiecutter.project_slug}}/README.md +334 -0
  15. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/.env.example +32 -0
  16. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/alembic/env.py +10 -1
  17. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/admin.py +1 -1
  18. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/__init__.py +31 -0
  19. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/crewai_assistant.py +563 -0
  20. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/deepagents_assistant.py +526 -0
  21. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langchain_assistant.py +4 -3
  22. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/agents/langgraph_assistant.py +371 -0
  23. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/agent.py +1472 -0
  24. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/api/routes/v1/oauth.py +3 -7
  25. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/cleanup.py +2 -2
  26. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/commands/seed.py +7 -2
  27. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/core/config.py +44 -7
  28. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/__init__.py +7 -0
  29. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/base.py +42 -0
  30. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/conversation.py +262 -1
  31. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/item.py +76 -1
  32. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/session.py +118 -1
  33. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/user.py +158 -1
  34. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/db/models/webhook.py +185 -3
  35. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/main.py +29 -2
  36. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/base.py +6 -0
  37. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/repositories/session.py +4 -4
  38. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/conversation.py +9 -9
  39. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/session.py +6 -6
  40. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/services/webhook.py +7 -7
  41. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/__init__.py +1 -1
  42. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/arq_app.py +165 -0
  43. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/app/worker/tasks/__init__.py +10 -1
  44. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/pyproject.toml +40 -0
  45. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/api/test_metrics.py +53 -0
  46. fastapi_gen/template/{{cookiecutter.project_slug}}/backend/tests/test_agents.py +2 -0
  47. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.dev.yml +6 -0
  48. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.prod.yml +100 -0
  49. fastapi_gen/template/{{cookiecutter.project_slug}}/docker-compose.yml +39 -0
  50. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/.env.example +5 -0
  51. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/chat-container.tsx +28 -1
  52. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/index.ts +1 -0
  53. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-item.tsx +22 -4
  54. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/message-list.tsx +23 -3
  55. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/components/chat/tool-approval-dialog.tsx +138 -0
  56. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-chat.ts +242 -18
  57. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/hooks/use-local-chat.ts +242 -17
  58. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/lib/constants.ts +1 -1
  59. fastapi_gen/template/{{cookiecutter.project_slug}}/frontend/src/types/chat.ts +57 -1
  60. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/configmap.yaml +63 -0
  61. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/deployment.yaml +242 -0
  62. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/ingress.yaml +44 -0
  63. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/kustomization.yaml +28 -0
  64. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/namespace.yaml +12 -0
  65. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/secret.yaml +59 -0
  66. fastapi_gen/template/{{cookiecutter.project_slug}}/kubernetes/service.yaml +23 -0
  67. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/nginx.conf +225 -0
  68. fastapi_gen/template/{{cookiecutter.project_slug}}/nginx/ssl/.gitkeep +18 -0
  69. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/WHEEL +0 -0
  70. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/entry_points.txt +0 -0
  71. {fastapi_fullstack-0.1.7.dist-info → fastapi_fullstack-0.1.15.dist-info}/licenses/LICENSE +0 -0
@@ -47,6 +47,12 @@ services:
47
47
  {%- endif %}
48
48
  {%- endif %}
49
49
  restart: unless-stopped
50
+ {%- if cookiecutter.enable_prometheus %}
51
+ labels:
52
+ - "prometheus.scrape=true"
53
+ - "prometheus.port={{ cookiecutter.backend_port }}"
54
+ - "prometheus.path=/metrics"
55
+ {%- endif %}
50
56
 
51
57
  {%- if cookiecutter.use_postgresql %}
52
58
 
@@ -222,6 +228,39 @@ services:
222
228
  restart: unless-stopped
223
229
  {%- endif %}
224
230
 
231
+ {%- if cookiecutter.use_arq %}
232
+
233
+ arq_worker:
234
+ build:
235
+ context: ./backend
236
+ dockerfile: Dockerfile
237
+ container_name: {{ cookiecutter.project_slug }}_arq_worker
238
+ volumes:
239
+ - ./backend/app:/app/app:ro
240
+ command: arq app.worker.arq_app.WorkerSettings
241
+ env_file:
242
+ - ./backend/.env
243
+ environment:
244
+ - DEBUG=true
245
+ {%- if cookiecutter.use_postgresql %}
246
+ - POSTGRES_HOST=db
247
+ {%- endif %}
248
+ - REDIS_HOST=redis
249
+ - ARQ_REDIS_HOST=redis
250
+ - ARQ_REDIS_PORT=6379
251
+ - ARQ_REDIS_DB=2
252
+ networks:
253
+ - backend
254
+ depends_on:
255
+ redis:
256
+ condition: service_healthy
257
+ {%- if cookiecutter.use_postgresql %}
258
+ db:
259
+ condition: service_healthy
260
+ {%- endif %}
261
+ restart: unless-stopped
262
+ {%- endif %}
263
+
225
264
  networks:
226
265
  backend:
227
266
  driver: bridge
@@ -3,6 +3,11 @@ BACKEND_URL=http://localhost:{{ cookiecutter.backend_port }}
3
3
 
4
4
  # WebSocket URL for real-time features
5
5
  BACKEND_WS_URL=ws://localhost:{{ cookiecutter.backend_port }}
6
+ {%- if cookiecutter.enable_oauth %}
7
+
8
+ # Public API URL for OAuth redirects (exposed to browser)
9
+ NEXT_PUBLIC_API_URL=http://localhost:{{ cookiecutter.backend_port }}
10
+ {%- endif %}
6
11
  {%- if cookiecutter.enable_logfire %}
7
12
 
8
13
  # Logfire/OpenTelemetry (server-side instrumentation)
@@ -4,8 +4,10 @@ import { useEffect, useRef, useCallback } from "react";
4
4
  import { useChat, useLocalChat } from "@/hooks";
5
5
  import { MessageList } from "./message-list";
6
6
  import { ChatInput } from "./chat-input";
7
+ import { ToolApprovalDialog } from "./tool-approval-dialog";
7
8
  import { Button } from "@/components/ui";
8
9
  import { Wifi, WifiOff, RotateCcw, Bot } from "lucide-react";
10
+ import type { PendingApproval, Decision } from "@/types";
9
11
  {%- if cookiecutter.enable_conversation_persistence and cookiecutter.use_database %}
10
12
  import { useConversationStore, useChatStore, useAuthStore } from "@/stores";
11
13
  import { useConversations } from "@/hooks";
@@ -46,6 +48,8 @@ function AuthenticatedChatContainer() {
46
48
  disconnect,
47
49
  sendMessage,
48
50
  clearMessages,
51
+ pendingApproval,
52
+ sendResumeDecisions,
49
53
  } = useChat({
50
54
  conversationId: currentConversationId,
51
55
  onConversationCreated: handleConversationCreated,
@@ -118,6 +122,8 @@ function AuthenticatedChatContainer() {
118
122
  sendMessage={sendMessage}
119
123
  clearMessages={clearMessages}
120
124
  messagesEndRef={messagesEndRef}
125
+ pendingApproval={pendingApproval}
126
+ onResumeDecisions={sendResumeDecisions}
121
127
  />
122
128
  );
123
129
  }
@@ -132,6 +138,8 @@ function LocalChatContainer() {
132
138
  disconnect,
133
139
  sendMessage,
134
140
  clearMessages,
141
+ pendingApproval,
142
+ sendResumeDecisions,
135
143
  } = useLocalChat();
136
144
 
137
145
  const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -153,6 +161,8 @@ function LocalChatContainer() {
153
161
  sendMessage={sendMessage}
154
162
  clearMessages={clearMessages}
155
163
  messagesEndRef={messagesEndRef}
164
+ pendingApproval={pendingApproval}
165
+ onResumeDecisions={sendResumeDecisions}
156
166
  />
157
167
  );
158
168
  }
@@ -170,6 +180,9 @@ interface ChatUIProps {
170
180
  sendMessage: (content: string) => void;
171
181
  clearMessages: () => void;
172
182
  messagesEndRef: React.RefObject<HTMLDivElement | null>;
183
+ // Human-in-the-Loop support
184
+ pendingApproval?: PendingApproval | null;
185
+ onResumeDecisions?: (decisions: Decision[]) => void;
173
186
  }
174
187
 
175
188
  function ChatUI({
@@ -179,6 +192,8 @@ function ChatUI({
179
192
  sendMessage,
180
193
  clearMessages,
181
194
  messagesEndRef,
195
+ pendingApproval,
196
+ onResumeDecisions,
182
197
  }: ChatUIProps) {
183
198
  return (
184
199
  <div className="flex flex-col h-full max-w-4xl mx-auto w-full">
@@ -199,11 +214,23 @@ function ChatUI({
199
214
  <div ref={messagesEndRef} />
200
215
  </div>
201
216
 
217
+ {/* Human-in-the-Loop: Tool Approval Dialog */}
218
+ {pendingApproval && onResumeDecisions && (
219
+ <div className="px-2 pb-2 sm:px-4 sm:pb-2">
220
+ <ToolApprovalDialog
221
+ actionRequests={pendingApproval.actionRequests}
222
+ reviewConfigs={pendingApproval.reviewConfigs}
223
+ onDecisions={onResumeDecisions}
224
+ disabled={!isConnected}
225
+ />
226
+ </div>
227
+ )}
228
+
202
229
  <div className="px-2 pb-2 sm:px-4 sm:pb-4">
203
230
  <div className="rounded-xl border bg-card shadow-sm p-3 sm:p-4">
204
231
  <ChatInput
205
232
  onSend={sendMessage}
206
- disabled={!isConnected || isProcessing}
233
+ disabled={!isConnected || isProcessing || !!pendingApproval}
207
234
  isProcessing={isProcessing}
208
235
  />
209
236
  <div className="flex items-center justify-between mt-3 pt-3 border-t">
@@ -2,6 +2,7 @@ export { ChatContainer } from "./chat-container";
2
2
  export { MessageList } from "./message-list";
3
3
  export { MessageItem } from "./message-item";
4
4
  export { ToolCallCard } from "./tool-call-card";
5
+ export { ToolApprovalDialog } from "./tool-approval-dialog";
5
6
  export { ChatInput } from "./chat-input";
6
7
  export { LocalConversationSidebar, ChatSidebarToggle } from "./local-conversation-sidebar";
7
8
  export { CopyButton } from "./copy-button";
@@ -9,22 +9,40 @@ import { User, Bot } from "lucide-react";
9
9
 
10
10
  interface MessageItemProps {
11
11
  message: ChatMessage;
12
+ groupPosition?: "first" | "middle" | "last" | "single";
12
13
  }
13
14
 
14
- export function MessageItem({ message }: MessageItemProps) {
15
+ export function MessageItem({ message, groupPosition }: MessageItemProps) {
15
16
  const isUser = message.role === "user";
17
+ const isGrouped = groupPosition && groupPosition !== "single";
16
18
 
17
19
  return (
18
20
  <div
19
21
  className={cn(
20
- "group flex gap-2 sm:gap-4 py-3 sm:py-4",
22
+ "group flex gap-2 sm:gap-4 relative overflow-visible",
23
+ isGrouped ? "py-2 sm:py-3" : "py-3 sm:py-4",
21
24
  isUser && "flex-row-reverse"
22
25
  )}
23
26
  >
27
+ {/* Timeline connector line for grouped messages */}
28
+ {isGrouped && !isUser && (
29
+ <div
30
+ className="absolute left-[15px] sm:left-[17px] w-0.5 bg-orange-500/40"
31
+ style={
32
+ groupPosition === "first"
33
+ ? { top: "24px", bottom: "0" }
34
+ : groupPosition === "last"
35
+ ? { top: "0", height: "24px" }
36
+ : { top: "0", bottom: "0" }
37
+ }
38
+ />
39
+ )}
40
+
24
41
  <div
25
42
  className={cn(
26
- "flex-shrink-0 w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center",
27
- isUser ? "bg-primary text-primary-foreground" : "bg-orange-500/10 text-orange-500"
43
+ "flex-shrink-0 w-8 h-8 sm:w-9 sm:h-9 rounded-full flex items-center justify-center z-10",
44
+ isUser ? "bg-primary text-primary-foreground" : "bg-orange-500/10 text-orange-500",
45
+ isGrouped && !isUser && "ring-2 ring-background"
28
46
  )}
29
47
  >
30
48
  {isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4 sm:h-5 sm:w-5" />}
@@ -8,10 +8,30 @@ interface MessageListProps {
8
8
  }
9
9
 
10
10
  export function MessageList({ messages }: MessageListProps) {
11
+ // Calculate group positions for timeline connector
12
+ const getGroupPosition = (
13
+ message: ChatMessage,
14
+ index: number
15
+ ): "first" | "middle" | "last" | "single" | undefined => {
16
+ if (!message.groupId) return undefined;
17
+
18
+ const groupMessages = messages.filter((m) => m.groupId === message.groupId);
19
+ if (groupMessages.length <= 1) return "single";
20
+
21
+ const groupIndex = groupMessages.findIndex((m) => m.id === message.id);
22
+ if (groupIndex === 0) return "first";
23
+ if (groupIndex === groupMessages.length - 1) return "last";
24
+ return "middle";
25
+ };
26
+
11
27
  return (
12
- <div className="space-y-4">
13
- {messages.map((message) => (
14
- <MessageItem key={message.id} message={message} />
28
+ <div className="space-y-0">
29
+ {messages.map((message, index) => (
30
+ <MessageItem
31
+ key={message.id}
32
+ message={message}
33
+ groupPosition={getGroupPosition(message, index)}
34
+ />
15
35
  ))}
16
36
  </div>
17
37
  );
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui";
5
+ import type { ActionRequest, ReviewConfig, Decision } from "@/types";
6
+ import { Wrench, AlertTriangle } from "lucide-react";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ interface ToolApprovalDialogProps {
10
+ actionRequests: ActionRequest[];
11
+ reviewConfigs: ReviewConfig[];
12
+ onDecisions: (decisions: Decision[]) => void;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export function ToolApprovalDialog({
17
+ actionRequests,
18
+ reviewConfigs,
19
+ onDecisions,
20
+ disabled = false,
21
+ }: ToolApprovalDialogProps) {
22
+ // Store edited args for each action
23
+ const [editedArgs, setEditedArgs] = useState<Record<string, string>>(() =>
24
+ Object.fromEntries(
25
+ actionRequests.map((a) => [a.id, JSON.stringify(a.args, null, 2)])
26
+ )
27
+ );
28
+ const [hasChanges, setHasChanges] = useState(false);
29
+
30
+ const handleArgsChange = (id: string, text: string) => {
31
+ setEditedArgs((prev) => ({ ...prev, [id]: text }));
32
+ setHasChanges(true);
33
+ };
34
+
35
+ const handleCancel = () => {
36
+ // Reset to original args
37
+ setEditedArgs(
38
+ Object.fromEntries(
39
+ actionRequests.map((a) => [a.id, JSON.stringify(a.args, null, 2)])
40
+ )
41
+ );
42
+ setHasChanges(false);
43
+ };
44
+
45
+ const handleSave = () => {
46
+ // Validate all JSON
47
+ for (const id of Object.keys(editedArgs)) {
48
+ try {
49
+ JSON.parse(editedArgs[id]);
50
+ } catch {
51
+ return; // Invalid JSON, don't save
52
+ }
53
+ }
54
+ setHasChanges(false);
55
+ };
56
+
57
+ const handleSubmit = () => {
58
+ const decisions: Decision[] = actionRequests.map((a) => {
59
+ try {
60
+ const parsed = JSON.parse(editedArgs[a.id]);
61
+ const original = JSON.stringify(a.args);
62
+ const edited = JSON.stringify(parsed);
63
+
64
+ if (original !== edited) {
65
+ return {
66
+ type: "edit" as const,
67
+ editedAction: { id: a.id, tool_name: a.tool_name, args: parsed },
68
+ };
69
+ }
70
+ return { type: "approve" as const };
71
+ } catch {
72
+ return { type: "reject" as const };
73
+ }
74
+ });
75
+ onDecisions(decisions);
76
+ };
77
+
78
+ return (
79
+ <div className="rounded-lg border border-yellow-500/50 bg-yellow-50/5 p-3 space-y-3">
80
+ <div className="flex items-center gap-2 text-sm text-yellow-600">
81
+ <AlertTriangle className="h-4 w-4" />
82
+ <span className="font-medium">Tool approval required</span>
83
+ </div>
84
+
85
+ {actionRequests.map((action) => (
86
+ <div key={action.id} className="space-y-1.5">
87
+ <div className="flex items-center gap-2">
88
+ <Wrench className="h-3.5 w-3.5 text-muted-foreground" />
89
+ <code className="text-xs font-semibold">{action.tool_name}</code>
90
+ </div>
91
+ <textarea
92
+ className={cn(
93
+ "w-full p-2 text-xs font-mono bg-background border rounded resize-none",
94
+ "min-h-[80px] max-h-[200px]"
95
+ )}
96
+ value={editedArgs[action.id]}
97
+ onChange={(e) => handleArgsChange(action.id, e.target.value)}
98
+ disabled={disabled}
99
+ rows={Math.min(10, (editedArgs[action.id]?.split("\n").length || 3) + 1)}
100
+ />
101
+ </div>
102
+ ))}
103
+
104
+ <div className="flex justify-end gap-2 pt-1 border-t">
105
+ {hasChanges && (
106
+ <>
107
+ <Button
108
+ size="sm"
109
+ variant="ghost"
110
+ className="h-7 text-xs"
111
+ onClick={handleCancel}
112
+ disabled={disabled}
113
+ >
114
+ Cancel
115
+ </Button>
116
+ <Button
117
+ size="sm"
118
+ variant="outline"
119
+ className="h-7 text-xs"
120
+ onClick={handleSave}
121
+ disabled={disabled}
122
+ >
123
+ Save
124
+ </Button>
125
+ </>
126
+ )}
127
+ <Button
128
+ size="sm"
129
+ className="h-7 text-xs"
130
+ onClick={handleSubmit}
131
+ disabled={disabled}
132
+ >
133
+ Submit ({actionRequests.length})
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ );
138
+ }