update
Browse files- .DS_Store +0 -0
- client/src/components/ServiceStatus.jsx +109 -0
- client/src/contexts/ServiceStatusContext.jsx +102 -0
- client/src/main.jsx +14 -11
- client/src/pages/Home.jsx +34 -15
- client/vite.config.js +9 -0
- server/api/models.py +14 -1
- server/api/routes/health.py +121 -0
- server/core/setup.py +1 -1
- server/server.py +4 -0
- server/services/flux_client.py +42 -14
- server/services/mistral_client.py +16 -2
.DS_Store
CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
|
|
client/src/components/ServiceStatus.jsx
ADDED
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from "react";
|
2 |
+
import { Box, Typography } from "@mui/material";
|
3 |
+
import { styled } from "@mui/system";
|
4 |
+
import { useServiceStatus } from "../contexts/ServiceStatusContext";
|
5 |
+
|
6 |
+
const StatusDot = styled("div")(({ status }) => ({
|
7 |
+
width: 8,
|
8 |
+
height: 8,
|
9 |
+
borderRadius: "50%",
|
10 |
+
backgroundColor:
|
11 |
+
status === "healthy"
|
12 |
+
? "#4caf50"
|
13 |
+
: status === "initializing"
|
14 |
+
? "#FFA726"
|
15 |
+
: status === "unhealthy"
|
16 |
+
? "#f44336"
|
17 |
+
: "#9e9e9e",
|
18 |
+
marginRight: 8,
|
19 |
+
}));
|
20 |
+
|
21 |
+
const ServiceLabel = styled(Box)({
|
22 |
+
display: "flex",
|
23 |
+
alignItems: "center",
|
24 |
+
marginRight: 16,
|
25 |
+
"& .MuiTypography-root": {
|
26 |
+
fontSize: "0.75rem",
|
27 |
+
color: "rgba(255, 255, 255, 0.7)",
|
28 |
+
},
|
29 |
+
});
|
30 |
+
|
31 |
+
const ServiceStatusContainer = styled("div")({
|
32 |
+
position: "absolute",
|
33 |
+
top: 16,
|
34 |
+
left: 16,
|
35 |
+
padding: 8,
|
36 |
+
borderRadius: 4,
|
37 |
+
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
38 |
+
zIndex: 1000,
|
39 |
+
});
|
40 |
+
|
41 |
+
export function ServiceStatus() {
|
42 |
+
const { services } = useServiceStatus();
|
43 |
+
|
44 |
+
const fetchServiceHealth = async (service) => {
|
45 |
+
try {
|
46 |
+
const response = await fetch(`/api/health/${service}`);
|
47 |
+
const data = await response.json();
|
48 |
+
|
49 |
+
if (response.ok) {
|
50 |
+
return {
|
51 |
+
status: data.status,
|
52 |
+
latency: data.latency,
|
53 |
+
error: null,
|
54 |
+
};
|
55 |
+
} else {
|
56 |
+
return {
|
57 |
+
status: data.status || "unhealthy",
|
58 |
+
latency: null,
|
59 |
+
error: data.error || "Service unavailable",
|
60 |
+
};
|
61 |
+
}
|
62 |
+
} catch (error) {
|
63 |
+
console.error(`Error checking ${service} health:`, error);
|
64 |
+
return {
|
65 |
+
status: "unhealthy",
|
66 |
+
latency: null,
|
67 |
+
error: error.message,
|
68 |
+
};
|
69 |
+
}
|
70 |
+
};
|
71 |
+
|
72 |
+
useEffect(() => {
|
73 |
+
const checkHealth = async () => {
|
74 |
+
const mistralHealth = await fetchServiceHealth("mistral");
|
75 |
+
const fluxHealth = await fetchServiceHealth("flux");
|
76 |
+
|
77 |
+
// Assuming you want to update the state with the fetched health data
|
78 |
+
// This is a placeholder and should be replaced with actual state management logic
|
79 |
+
console.log("Updating health status:", {
|
80 |
+
mistral: mistralHealth,
|
81 |
+
flux: fluxHealth,
|
82 |
+
});
|
83 |
+
};
|
84 |
+
|
85 |
+
// Initial check
|
86 |
+
checkHealth();
|
87 |
+
|
88 |
+
// Check every 30 seconds
|
89 |
+
const interval = setInterval(checkHealth, 30000);
|
90 |
+
|
91 |
+
return () => clearInterval(interval);
|
92 |
+
}, []);
|
93 |
+
|
94 |
+
return (
|
95 |
+
<ServiceStatusContainer>
|
96 |
+
{Object.entries(services).map(([service, { status, latency, error }]) => (
|
97 |
+
<ServiceLabel key={service}>
|
98 |
+
<StatusDot status={status} />
|
99 |
+
<Typography>
|
100 |
+
{service.charAt(0).toUpperCase() + service.slice(1)}
|
101 |
+
{/* {status === "healthy" && latency && ` (${Math.round(latency)}ms)`} */}
|
102 |
+
{/* {status === "initializing" && " (initializing...)"}
|
103 |
+
{status === "unhealthy" && error && ` (${error})`} */}
|
104 |
+
</Typography>
|
105 |
+
</ServiceLabel>
|
106 |
+
))}
|
107 |
+
</ServiceStatusContainer>
|
108 |
+
);
|
109 |
+
}
|
client/src/contexts/ServiceStatusContext.jsx
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { createContext, useContext, useState, useEffect } from "react";
|
2 |
+
|
3 |
+
const ServiceStatusContext = createContext();
|
4 |
+
|
5 |
+
export function ServiceStatusProvider({ children }) {
|
6 |
+
const [services, setServices] = useState({
|
7 |
+
mistral: { status: "loading", latency: null },
|
8 |
+
flux: { status: "loading", latency: null },
|
9 |
+
});
|
10 |
+
|
11 |
+
const areServicesHealthy = () => {
|
12 |
+
return Object.values(services).every(
|
13 |
+
(service) => service.status === "healthy"
|
14 |
+
);
|
15 |
+
};
|
16 |
+
|
17 |
+
const fetchServiceHealth = async (service) => {
|
18 |
+
console.log(`Checking health for ${service} service...`);
|
19 |
+
try {
|
20 |
+
const response = await fetch(`/api/health/${service}`, {
|
21 |
+
method: "GET",
|
22 |
+
headers: {
|
23 |
+
Accept: "application/json",
|
24 |
+
},
|
25 |
+
});
|
26 |
+
console.log(`Response status for ${service}:`, response.status);
|
27 |
+
|
28 |
+
const data = await response.json();
|
29 |
+
console.log(`Health data for ${service}:`, data);
|
30 |
+
|
31 |
+
if (response.ok) {
|
32 |
+
setServices((prev) => ({
|
33 |
+
...prev,
|
34 |
+
[service]: {
|
35 |
+
status: data.status,
|
36 |
+
latency: data.latency,
|
37 |
+
error: data.error,
|
38 |
+
},
|
39 |
+
}));
|
40 |
+
} else {
|
41 |
+
const errorData = data?.detail || data;
|
42 |
+
console.error(`Error checking ${service} health:`, errorData);
|
43 |
+
setServices((prev) => ({
|
44 |
+
...prev,
|
45 |
+
[service]: {
|
46 |
+
status: "unhealthy",
|
47 |
+
latency: null,
|
48 |
+
error: errorData?.error || "Service unavailable",
|
49 |
+
},
|
50 |
+
}));
|
51 |
+
}
|
52 |
+
} catch (error) {
|
53 |
+
console.error(`Failed to check ${service} health:`, error);
|
54 |
+
setServices((prev) => ({
|
55 |
+
...prev,
|
56 |
+
[service]: {
|
57 |
+
status: "unhealthy",
|
58 |
+
latency: null,
|
59 |
+
error: "Connection error",
|
60 |
+
},
|
61 |
+
}));
|
62 |
+
}
|
63 |
+
};
|
64 |
+
|
65 |
+
useEffect(() => {
|
66 |
+
console.log("ServiceStatusProvider mounted, initializing health checks...");
|
67 |
+
const checkHealth = () => {
|
68 |
+
console.log("Running health checks...");
|
69 |
+
fetchServiceHealth("mistral");
|
70 |
+
fetchServiceHealth("flux");
|
71 |
+
};
|
72 |
+
|
73 |
+
// Premier check immédiat
|
74 |
+
checkHealth();
|
75 |
+
|
76 |
+
// Mettre en place l'intervalle
|
77 |
+
console.log("Setting up health check interval...");
|
78 |
+
const interval = setInterval(checkHealth, 30000);
|
79 |
+
|
80 |
+
// Cleanup
|
81 |
+
return () => {
|
82 |
+
console.log("Cleaning up health check interval...");
|
83 |
+
clearInterval(interval);
|
84 |
+
};
|
85 |
+
}, []);
|
86 |
+
|
87 |
+
return (
|
88 |
+
<ServiceStatusContext.Provider value={{ services, areServicesHealthy }}>
|
89 |
+
{children}
|
90 |
+
</ServiceStatusContext.Provider>
|
91 |
+
);
|
92 |
+
}
|
93 |
+
|
94 |
+
export function useServiceStatus() {
|
95 |
+
const context = useContext(ServiceStatusContext);
|
96 |
+
if (!context) {
|
97 |
+
throw new Error(
|
98 |
+
"useServiceStatus must be used within a ServiceStatusProvider"
|
99 |
+
);
|
100 |
+
}
|
101 |
+
return context;
|
102 |
+
}
|
client/src/main.jsx
CHANGED
@@ -10,6 +10,7 @@ import { Tutorial } from "./pages/Tutorial";
|
|
10 |
import Debug from "./pages/Debug";
|
11 |
import { Universe } from "./pages/Universe";
|
12 |
import { SoundProvider } from "./contexts/SoundContext";
|
|
|
13 |
import { GameNavigation } from "./components/GameNavigation";
|
14 |
import { AppBackground } from "./components/AppBackground";
|
15 |
import "./index.css";
|
@@ -18,17 +19,19 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
|
18 |
<ThemeProvider theme={theme}>
|
19 |
<CssBaseline />
|
20 |
<SoundProvider>
|
21 |
-
<
|
22 |
-
<
|
23 |
-
|
24 |
-
|
25 |
-
<
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
|
|
|
|
32 |
</SoundProvider>
|
33 |
</ThemeProvider>
|
34 |
);
|
|
|
10 |
import Debug from "./pages/Debug";
|
11 |
import { Universe } from "./pages/Universe";
|
12 |
import { SoundProvider } from "./contexts/SoundContext";
|
13 |
+
import { ServiceStatusProvider } from "./contexts/ServiceStatusContext";
|
14 |
import { GameNavigation } from "./components/GameNavigation";
|
15 |
import { AppBackground } from "./components/AppBackground";
|
16 |
import "./index.css";
|
|
|
19 |
<ThemeProvider theme={theme}>
|
20 |
<CssBaseline />
|
21 |
<SoundProvider>
|
22 |
+
<ServiceStatusProvider>
|
23 |
+
<BrowserRouter>
|
24 |
+
<GameNavigation />
|
25 |
+
<AppBackground />
|
26 |
+
<Routes>
|
27 |
+
<Route path="/" element={<Home />} />
|
28 |
+
<Route path="/game" element={<Game />} />
|
29 |
+
<Route path="/tutorial" element={<Tutorial />} />
|
30 |
+
<Route path="/debug" element={<Debug />} />
|
31 |
+
<Route path="/universe" element={<Universe />} />
|
32 |
+
</Routes>
|
33 |
+
</BrowserRouter>
|
34 |
+
</ServiceStatusProvider>
|
35 |
</SoundProvider>
|
36 |
</ThemeProvider>
|
37 |
);
|
client/src/pages/Home.jsx
CHANGED
@@ -4,16 +4,20 @@ import {
|
|
4 |
Typography,
|
5 |
useTheme,
|
6 |
useMediaQuery,
|
|
|
7 |
} from "@mui/material";
|
8 |
import { motion } from "framer-motion";
|
9 |
import { useNavigate } from "react-router-dom";
|
10 |
import { useSoundSystem } from "../contexts/SoundContext";
|
|
|
11 |
import { BlinkingText } from "../components/BlinkingText";
|
12 |
import { BookPages } from "../components/BookPages";
|
|
|
13 |
|
14 |
export function Home() {
|
15 |
const navigate = useNavigate();
|
16 |
const { playSound } = useSoundSystem();
|
|
|
17 |
const theme = useTheme();
|
18 |
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
19 |
|
@@ -22,6 +26,24 @@ export function Home() {
|
|
22 |
navigate("/tutorial");
|
23 |
};
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
return (
|
26 |
<motion.div
|
27 |
initial={{ opacity: 0 }}
|
@@ -35,6 +57,7 @@ export function Home() {
|
|
35 |
overflow: "hidden",
|
36 |
}}
|
37 |
>
|
|
|
38 |
<Box
|
39 |
sx={{
|
40 |
display: "flex",
|
@@ -43,7 +66,7 @@ export function Home() {
|
|
43 |
justifyContent: "center",
|
44 |
minHeight: "100vh",
|
45 |
height: "100%",
|
46 |
-
width: isMobile ? "80%" : "40%",
|
47 |
margin: "auto",
|
48 |
position: "relative",
|
49 |
}}
|
@@ -96,20 +119,16 @@ export function Home() {
|
|
96 |
Experience a unique comic book where artificial intelligence brings
|
97 |
your choices to life, shaping the narrative as you explore.
|
98 |
</Typography>
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
}}
|
110 |
-
>
|
111 |
-
Play
|
112 |
-
</Button>
|
113 |
</Box>
|
114 |
</motion.div>
|
115 |
);
|
|
|
4 |
Typography,
|
5 |
useTheme,
|
6 |
useMediaQuery,
|
7 |
+
Tooltip,
|
8 |
} from "@mui/material";
|
9 |
import { motion } from "framer-motion";
|
10 |
import { useNavigate } from "react-router-dom";
|
11 |
import { useSoundSystem } from "../contexts/SoundContext";
|
12 |
+
import { useServiceStatus } from "../contexts/ServiceStatusContext";
|
13 |
import { BlinkingText } from "../components/BlinkingText";
|
14 |
import { BookPages } from "../components/BookPages";
|
15 |
+
import { ServiceStatus } from "../components/ServiceStatus";
|
16 |
|
17 |
export function Home() {
|
18 |
const navigate = useNavigate();
|
19 |
const { playSound } = useSoundSystem();
|
20 |
+
const { areServicesHealthy } = useServiceStatus();
|
21 |
const theme = useTheme();
|
22 |
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
23 |
|
|
|
26 |
navigate("/tutorial");
|
27 |
};
|
28 |
|
29 |
+
const playButton = (
|
30 |
+
<Button
|
31 |
+
color="primary"
|
32 |
+
size="large"
|
33 |
+
variant="contained"
|
34 |
+
onClick={handlePlay}
|
35 |
+
disabled={!areServicesHealthy()}
|
36 |
+
sx={{
|
37 |
+
mt: 4,
|
38 |
+
fontSize: isMobile ? "1rem" : "1.2rem",
|
39 |
+
padding: isMobile ? "8px 24px" : "12px 36px",
|
40 |
+
zIndex: 10,
|
41 |
+
}}
|
42 |
+
>
|
43 |
+
Play
|
44 |
+
</Button>
|
45 |
+
);
|
46 |
+
|
47 |
return (
|
48 |
<motion.div
|
49 |
initial={{ opacity: 0 }}
|
|
|
57 |
overflow: "hidden",
|
58 |
}}
|
59 |
>
|
60 |
+
<ServiceStatus />
|
61 |
<Box
|
62 |
sx={{
|
63 |
display: "flex",
|
|
|
66 |
justifyContent: "center",
|
67 |
minHeight: "100vh",
|
68 |
height: "100%",
|
69 |
+
width: isMobile ? "80%" : "40%",
|
70 |
margin: "auto",
|
71 |
position: "relative",
|
72 |
}}
|
|
|
119 |
Experience a unique comic book where artificial intelligence brings
|
120 |
your choices to life, shaping the narrative as you explore.
|
121 |
</Typography>
|
122 |
+
{areServicesHealthy() ? (
|
123 |
+
playButton
|
124 |
+
) : (
|
125 |
+
<Tooltip
|
126 |
+
title="Services are currently unavailable. Please wait..."
|
127 |
+
arrow
|
128 |
+
>
|
129 |
+
<span>{playButton}</span>
|
130 |
+
</Tooltip>
|
131 |
+
)}
|
|
|
|
|
|
|
|
|
132 |
</Box>
|
133 |
</motion.div>
|
134 |
);
|
client/vite.config.js
CHANGED
@@ -4,4 +4,13 @@ import react from "@vitejs/plugin-react";
|
|
4 |
// https://vite.dev/config/
|
5 |
export default defineConfig({
|
6 |
plugins: [react()],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
});
|
|
|
4 |
// https://vite.dev/config/
|
5 |
export default defineConfig({
|
6 |
plugins: [react()],
|
7 |
+
server: {
|
8 |
+
proxy: {
|
9 |
+
"/api": {
|
10 |
+
target: "http://localhost:8000",
|
11 |
+
changeOrigin: true,
|
12 |
+
secure: false,
|
13 |
+
},
|
14 |
+
},
|
15 |
+
},
|
16 |
});
|
server/api/models.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
from pydantic import BaseModel, Field, validator
|
2 |
-
from typing import List, Optional
|
3 |
from core.constants import GameConfig
|
|
|
|
|
4 |
|
5 |
class Choice(BaseModel):
|
6 |
id: int
|
@@ -82,3 +84,14 @@ class StoryResponse(BaseModel):
|
|
82 |
if len(v) != 2:
|
83 |
raise ValueError('Must have exactly 2 choices for story progression')
|
84 |
return v
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from pydantic import BaseModel, Field, validator
|
2 |
+
from typing import List, Optional, Dict, Any, Union
|
3 |
from core.constants import GameConfig
|
4 |
+
import re
|
5 |
+
from enum import Enum
|
6 |
|
7 |
class Choice(BaseModel):
|
8 |
id: int
|
|
|
84 |
if len(v) != 2:
|
85 |
raise ValueError('Must have exactly 2 choices for story progression')
|
86 |
return v
|
87 |
+
|
88 |
+
class ServiceStatus(str, Enum):
|
89 |
+
HEALTHY = "healthy"
|
90 |
+
UNHEALTHY = "unhealthy"
|
91 |
+
INITIALIZING = "initializing"
|
92 |
+
|
93 |
+
class HealthCheckResponse(BaseModel):
|
94 |
+
status: ServiceStatus
|
95 |
+
service: str
|
96 |
+
latency: Optional[float] = None
|
97 |
+
error: Optional[str] = None
|
server/api/routes/health.py
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
import asyncio
|
3 |
+
from fastapi import APIRouter, HTTPException
|
4 |
+
from langchain.schema import SystemMessage
|
5 |
+
from api.models import HealthCheckResponse
|
6 |
+
from services.mistral_client import MistralClient
|
7 |
+
from services.flux_client import FluxClient
|
8 |
+
|
9 |
+
def get_health_router(mistral_client: MistralClient, flux_client: FluxClient) -> APIRouter:
|
10 |
+
router = APIRouter()
|
11 |
+
|
12 |
+
@router.get("/health/mistral", response_model=HealthCheckResponse)
|
13 |
+
async def check_mistral_health():
|
14 |
+
"""Vérifie la disponibilité du service Mistral."""
|
15 |
+
print("Checking Mistral health...")
|
16 |
+
start_time = time.time()
|
17 |
+
try:
|
18 |
+
# Try to make a simple request to Mistral with a 5 second timeout
|
19 |
+
await asyncio.wait_for(
|
20 |
+
mistral_client.check_health(),
|
21 |
+
timeout=5.0
|
22 |
+
)
|
23 |
+
|
24 |
+
latency = (time.time() - start_time) * 1000 # Convert to milliseconds
|
25 |
+
print(f"Mistral health check successful. Latency: {latency}ms")
|
26 |
+
return HealthCheckResponse(
|
27 |
+
status="healthy",
|
28 |
+
service="mistral",
|
29 |
+
latency=latency
|
30 |
+
)
|
31 |
+
except asyncio.TimeoutError:
|
32 |
+
print("Mistral health check failed: timeout")
|
33 |
+
raise HTTPException(
|
34 |
+
status_code=503,
|
35 |
+
detail=HealthCheckResponse(
|
36 |
+
status="unhealthy",
|
37 |
+
service="mistral",
|
38 |
+
latency=None,
|
39 |
+
error="Request timed out after 5 seconds"
|
40 |
+
).dict()
|
41 |
+
)
|
42 |
+
except Exception as e:
|
43 |
+
print(f"Mistral health check failed: {str(e)}")
|
44 |
+
raise HTTPException(
|
45 |
+
status_code=503,
|
46 |
+
detail=HealthCheckResponse(
|
47 |
+
status="unhealthy",
|
48 |
+
service="mistral",
|
49 |
+
latency=None,
|
50 |
+
error=str(e)
|
51 |
+
).dict()
|
52 |
+
)
|
53 |
+
|
54 |
+
@router.get("/health/flux", response_model=HealthCheckResponse)
|
55 |
+
async def check_flux_health():
|
56 |
+
"""Vérifie la disponibilité du service Flux."""
|
57 |
+
print("Checking Flux health...")
|
58 |
+
start_time = time.time()
|
59 |
+
try:
|
60 |
+
# Try to generate a test image with a timeout
|
61 |
+
is_healthy, status = await asyncio.wait_for(
|
62 |
+
flux_client.check_health(),
|
63 |
+
timeout=5.0 # Même timeout que Mistral
|
64 |
+
)
|
65 |
+
|
66 |
+
latency = (time.time() - start_time) * 1000 # Convert to milliseconds
|
67 |
+
|
68 |
+
if is_healthy:
|
69 |
+
print(f"Flux health check successful. Latency: {latency}ms")
|
70 |
+
return HealthCheckResponse(
|
71 |
+
status="healthy",
|
72 |
+
service="flux",
|
73 |
+
latency=latency
|
74 |
+
)
|
75 |
+
elif status == "initializing":
|
76 |
+
print("Flux service is initializing")
|
77 |
+
raise HTTPException(
|
78 |
+
status_code=503,
|
79 |
+
detail=HealthCheckResponse(
|
80 |
+
status="initializing",
|
81 |
+
service="flux",
|
82 |
+
latency=None,
|
83 |
+
error="Service is initializing"
|
84 |
+
).dict()
|
85 |
+
)
|
86 |
+
else:
|
87 |
+
print(f"Flux health check failed: {status}")
|
88 |
+
raise HTTPException(
|
89 |
+
status_code=503,
|
90 |
+
detail=HealthCheckResponse(
|
91 |
+
status="unhealthy",
|
92 |
+
service="flux",
|
93 |
+
latency=None,
|
94 |
+
error=status
|
95 |
+
).dict()
|
96 |
+
)
|
97 |
+
|
98 |
+
except asyncio.TimeoutError:
|
99 |
+
print("Flux health check failed: timeout")
|
100 |
+
raise HTTPException(
|
101 |
+
status_code=503,
|
102 |
+
detail=HealthCheckResponse(
|
103 |
+
status="unhealthy",
|
104 |
+
service="flux",
|
105 |
+
latency=None,
|
106 |
+
error="Image generation timed out after 5 seconds"
|
107 |
+
).dict()
|
108 |
+
)
|
109 |
+
except Exception as e:
|
110 |
+
print(f"Flux health check failed: {str(e)}")
|
111 |
+
raise HTTPException(
|
112 |
+
status_code=503,
|
113 |
+
detail=HealthCheckResponse(
|
114 |
+
status="unhealthy",
|
115 |
+
service="flux",
|
116 |
+
latency=None,
|
117 |
+
error=str(e)
|
118 |
+
).dict()
|
119 |
+
)
|
120 |
+
|
121 |
+
return router
|
server/core/setup.py
CHANGED
@@ -5,7 +5,7 @@ from core.story_generator import StoryGenerator
|
|
5 |
# Initialize generators with None - they will be set up when needed
|
6 |
universe_generator = None
|
7 |
|
8 |
-
def setup_game(api_key: str, model_name: str = "mistral-
|
9 |
"""Setup all game components with the provided API key."""
|
10 |
global universe_generator
|
11 |
|
|
|
5 |
# Initialize generators with None - they will be set up when needed
|
6 |
universe_generator = None
|
7 |
|
8 |
+
def setup_game(api_key: str, model_name: str = "mistral-small"):
|
9 |
"""Setup all game components with the provided API key."""
|
10 |
global universe_generator
|
11 |
|
server/server.py
CHANGED
@@ -10,10 +10,12 @@ from core.story_generator import StoryGenerator
|
|
10 |
from core.setup import setup_game, get_universe_generator
|
11 |
from core.session_manager import SessionManager
|
12 |
from services.flux_client import FluxClient
|
|
|
13 |
from api.routes.chat import get_chat_router
|
14 |
from api.routes.image import get_image_router
|
15 |
from api.routes.speech import get_speech_router
|
16 |
from api.routes.universe import get_universe_router
|
|
|
17 |
|
18 |
# Load environment variables
|
19 |
load_dotenv()
|
@@ -52,6 +54,7 @@ print("Creating global SessionManager")
|
|
52 |
session_manager = SessionManager()
|
53 |
story_generator = StoryGenerator(api_key=mistral_api_key)
|
54 |
flux_client = FluxClient(api_key=HF_API_KEY)
|
|
|
55 |
|
56 |
# Health check endpoint
|
57 |
@app.get("/api/health")
|
@@ -65,6 +68,7 @@ app.include_router(get_chat_router(session_manager, story_generator), prefix="/a
|
|
65 |
app.include_router(get_image_router(flux_client), prefix="/api")
|
66 |
app.include_router(get_speech_router(), prefix="/api")
|
67 |
app.include_router(get_universe_router(session_manager, story_generator), prefix="/api")
|
|
|
68 |
|
69 |
@app.on_event("startup")
|
70 |
async def startup_event():
|
|
|
10 |
from core.setup import setup_game, get_universe_generator
|
11 |
from core.session_manager import SessionManager
|
12 |
from services.flux_client import FluxClient
|
13 |
+
from services.mistral_client import MistralClient
|
14 |
from api.routes.chat import get_chat_router
|
15 |
from api.routes.image import get_image_router
|
16 |
from api.routes.speech import get_speech_router
|
17 |
from api.routes.universe import get_universe_router
|
18 |
+
from api.routes.health import get_health_router
|
19 |
|
20 |
# Load environment variables
|
21 |
load_dotenv()
|
|
|
54 |
session_manager = SessionManager()
|
55 |
story_generator = StoryGenerator(api_key=mistral_api_key)
|
56 |
flux_client = FluxClient(api_key=HF_API_KEY)
|
57 |
+
mistral_client = MistralClient(api_key=mistral_api_key)
|
58 |
|
59 |
# Health check endpoint
|
60 |
@app.get("/api/health")
|
|
|
68 |
app.include_router(get_image_router(flux_client), prefix="/api")
|
69 |
app.include_router(get_speech_router(), prefix="/api")
|
70 |
app.include_router(get_universe_router(session_manager, story_generator), prefix="/api")
|
71 |
+
app.include_router(get_health_router(mistral_client, flux_client), prefix="/api")
|
72 |
|
73 |
@app.on_event("startup")
|
74 |
async def startup_event():
|
server/services/flux_client.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import os
|
2 |
import aiohttp
|
3 |
-
from typing import Optional
|
4 |
|
5 |
class FluxClient:
|
6 |
def __init__(self, api_key: str):
|
@@ -18,7 +18,7 @@ class FluxClient:
|
|
18 |
width: int,
|
19 |
height: int,
|
20 |
num_inference_steps: int = 5,
|
21 |
-
guidance_scale: float = 9.0) -> Optional[bytes]:
|
22 |
"""Génère une image à partir d'un prompt."""
|
23 |
try:
|
24 |
# Ensure dimensions are multiples of 8
|
@@ -29,7 +29,6 @@ class FluxClient:
|
|
29 |
print(f"Headers: Authorization: Bearer {self.api_key[:4]}...")
|
30 |
print(f"Request body: {prompt[:100]}...")
|
31 |
|
32 |
-
|
33 |
session = await self._get_session()
|
34 |
async with session.post(
|
35 |
self.endpoint,
|
@@ -50,30 +49,59 @@ class FluxClient:
|
|
50 |
) as response:
|
51 |
print(f"Response status code: {response.status}")
|
52 |
print(f"Response headers: {response.headers}")
|
53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
if response.status == 200:
|
56 |
content = await response.read()
|
57 |
-
|
58 |
-
print(f"Received successful response with content length: {content_length}")
|
59 |
-
if isinstance(content, bytes):
|
60 |
-
print("Response content is bytes (correct)")
|
61 |
-
else:
|
62 |
-
print(f"Warning: Response content is {type(content)}")
|
63 |
-
return content
|
64 |
else:
|
65 |
error_content = await response.text()
|
66 |
print(f"Error from Flux API: {response.status}")
|
67 |
print(f"Response content: {error_content}")
|
68 |
-
return None
|
69 |
|
70 |
except Exception as e:
|
71 |
print(f"Error in FluxClient.generate_image: {str(e)}")
|
72 |
import traceback
|
73 |
print(f"Traceback: {traceback.format_exc()}")
|
74 |
-
return None
|
75 |
|
76 |
async def close(self):
|
77 |
if self._session:
|
78 |
await self._session.close()
|
79 |
-
self._session = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import aiohttp
|
3 |
+
from typing import Optional, Tuple
|
4 |
|
5 |
class FluxClient:
|
6 |
def __init__(self, api_key: str):
|
|
|
18 |
width: int,
|
19 |
height: int,
|
20 |
num_inference_steps: int = 5,
|
21 |
+
guidance_scale: float = 9.0) -> Tuple[Optional[bytes], Optional[str]]:
|
22 |
"""Génère une image à partir d'un prompt."""
|
23 |
try:
|
24 |
# Ensure dimensions are multiples of 8
|
|
|
29 |
print(f"Headers: Authorization: Bearer {self.api_key[:4]}...")
|
30 |
print(f"Request body: {prompt[:100]}...")
|
31 |
|
|
|
32 |
session = await self._get_session()
|
33 |
async with session.post(
|
34 |
self.endpoint,
|
|
|
49 |
) as response:
|
50 |
print(f"Response status code: {response.status}")
|
51 |
print(f"Response headers: {response.headers}")
|
52 |
+
|
53 |
+
# Vérifier si le modèle est en cours d'initialisation
|
54 |
+
if response.status == 503:
|
55 |
+
error_content = await response.text()
|
56 |
+
if "currently loading" in error_content.lower() or "initializing" in error_content.lower():
|
57 |
+
return None, "initializing"
|
58 |
+
return None, "unavailable"
|
59 |
|
60 |
if response.status == 200:
|
61 |
content = await response.read()
|
62 |
+
return content, None
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
else:
|
64 |
error_content = await response.text()
|
65 |
print(f"Error from Flux API: {response.status}")
|
66 |
print(f"Response content: {error_content}")
|
67 |
+
return None, error_content
|
68 |
|
69 |
except Exception as e:
|
70 |
print(f"Error in FluxClient.generate_image: {str(e)}")
|
71 |
import traceback
|
72 |
print(f"Traceback: {traceback.format_exc()}")
|
73 |
+
return None, str(e)
|
74 |
|
75 |
async def close(self):
|
76 |
if self._session:
|
77 |
await self._session.close()
|
78 |
+
self._session = None
|
79 |
+
|
80 |
+
async def check_health(self) -> Tuple[bool, Optional[str]]:
|
81 |
+
"""
|
82 |
+
Vérifie la disponibilité du service Flux en tentant de générer une petite image.
|
83 |
+
|
84 |
+
Returns:
|
85 |
+
Tuple[bool, Optional[str]]: (is_healthy, status)
|
86 |
+
- is_healthy: True si le service est disponible
|
87 |
+
- status: "healthy", "initializing", ou message d'erreur
|
88 |
+
"""
|
89 |
+
try:
|
90 |
+
# Test simple prompt pour générer une petite image
|
91 |
+
test_image, status = await self.generate_image(
|
92 |
+
prompt="test image, simple circle",
|
93 |
+
width=64, # Petite image pour le test
|
94 |
+
height=64,
|
95 |
+
num_inference_steps=1 # Minimum d'étapes pour être rapide
|
96 |
+
)
|
97 |
+
|
98 |
+
if test_image is not None:
|
99 |
+
return True, "healthy"
|
100 |
+
elif status == "initializing":
|
101 |
+
return False, "initializing"
|
102 |
+
else:
|
103 |
+
return False, status or "unavailable"
|
104 |
+
|
105 |
+
except Exception as e:
|
106 |
+
print(f"Health check failed: {str(e)}")
|
107 |
+
return False, str(e)
|
server/services/mistral_client.py
CHANGED
@@ -47,7 +47,7 @@ class MistralValidationError(MistralAPIError):
|
|
47 |
pass
|
48 |
|
49 |
class MistralClient:
|
50 |
-
def __init__(self, api_key: str, model_name: str = "mistral-
|
51 |
logger.info(f"Initializing MistralClient with model: {model_name}, max_tokens: {max_tokens}")
|
52 |
self.model = ChatMistralAI(
|
53 |
mistral_api_key=api_key,
|
@@ -213,4 +213,18 @@ class MistralClient:
|
|
213 |
continue
|
214 |
|
215 |
logger.error(f"Failed after {self.max_retries} attempts. Last error: {last_error or str(e)}")
|
216 |
-
raise Exception(f"Failed after {self.max_retries} attempts. Last error: {last_error or str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
pass
|
48 |
|
49 |
class MistralClient:
|
50 |
+
def __init__(self, api_key: str, model_name: str = "mistral-small-latest", max_tokens: int = 1000):
|
51 |
logger.info(f"Initializing MistralClient with model: {model_name}, max_tokens: {max_tokens}")
|
52 |
self.model = ChatMistralAI(
|
53 |
mistral_api_key=api_key,
|
|
|
213 |
continue
|
214 |
|
215 |
logger.error(f"Failed after {self.max_retries} attempts. Last error: {last_error or str(e)}")
|
216 |
+
raise Exception(f"Failed after {self.max_retries} attempts. Last error: {last_error or str(e)}")
|
217 |
+
|
218 |
+
async def check_health(self) -> bool:
|
219 |
+
"""
|
220 |
+
Vérifie la disponibilité du service Mistral avec un appel simple sans retry.
|
221 |
+
|
222 |
+
Returns:
|
223 |
+
bool: True si le service est disponible, False sinon
|
224 |
+
"""
|
225 |
+
try:
|
226 |
+
response = await self.model.ainvoke([SystemMessage(content="Hi")])
|
227 |
+
return True
|
228 |
+
except Exception as e:
|
229 |
+
logger.error(f"Health check failed: {str(e)}")
|
230 |
+
raise
|