add KnowledgeWorkFlow system

This commit is contained in:
Robin COuret
2026-03-08 01:33:21 +01:00
parent 73fff0955b
commit d1e5a6b0c7
25 changed files with 351 additions and 174 deletions

View File

@@ -3,8 +3,10 @@ from fastapi import APIRouter
from .knowledges import router as knowledge_router from .knowledges import router as knowledge_router
from .metrics import router as metric_router from .metrics import router as metric_router
from .auth import router as auth_router from .auth import router as auth_router
from .questions import router as question_router
router = APIRouter(prefix="/v1") router = APIRouter(prefix="/v1")
router.include_router(knowledge_router) router.include_router(knowledge_router)
router.include_router(question_router)
router.include_router(metric_router) router.include_router(metric_router)
router.include_router(auth_router) router.include_router(auth_router)

View File

@@ -44,7 +44,7 @@ def generate_questions(id: int, current_user: Annotated[str, Depends(get_current
) )
questions_raw = questions_generation(knowledge) questions_raw = questions_generation(knowledge)
for q in questions_raw: for q in questions_raw:
question = Question(question = q, knowledge=knowledge) question = Question(question = q, knowledge=knowledge, user=current_user)
create_question(question) create_question(question)
return questions_raw return questions_raw

View File

@@ -8,8 +8,8 @@ from src.app.auth.dependancies import get_current_user
router = APIRouter(tags=["metrics"]) router = APIRouter(tags=["metrics"])
@router.post("/metrics/") # @router.post("/metrics/")
def create(metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]): # def create(metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
metric: Metric = Metric(question_id = metric_data.question_id, need_index = metric_data.need_index, user = current_user) # metric: Metric = Metric(question_id = metric_data.question_id, need_index = metric_data.need_index, user = current_user)
created_metric: Metric = create_metric(metric) # created_metric: Metric = create_metric(metric)
return created_metric # return created_metric

View File

@@ -0,0 +1,38 @@
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from src.app.auth.dependancies import get_current_user
from src.app.models.question import Question
from src.app.models.metric import Metric, MetricCreate
from src.app.data.question import get_question_by_id
from src.app.data.metric import get_metrics, create_metric
#Added in __ini__
router = APIRouter(tags=["questions"])
@router.get("/questions/{id}/metrics")
def read_questions(id: int, current_user: Annotated[str, Depends(get_current_user)]):
question: Question = get_question_by_id(id, current_user)
if not question:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
metrics = get_metrics(question)
return metrics
@router.post("/questions/{id}/metrics")
def create(id: int, metric_data: MetricCreate, current_user: Annotated[str, Depends(get_current_user)]):
question: Question = get_question_by_id(id, current_user)
if not question:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden. The requested knowledge is not available for the provided ID.",
headers={"WWW-Authenticate": "Bearer"},
)
metric: Metric = Metric(question_id = id, need_index = metric_data.need_index, user = current_user)
created_metric: Metric = create_metric(metric)
return created_metric

View File

@@ -1,13 +1,21 @@
from sqlmodel import Session, select from sqlmodel import Session, select
from src.app.models.question import Question from src.app.models.question import Question
from src.app.models.user import User
from src.app.database import engine from src.app.database import engine
def get_question_by_id(question_id: int): def get_question_by_id(question_id: int, user: User):
with Session(engine) as session: with Session(engine) as session:
question = session.get(Question, question_id) statement = select(Question).where(Question.id == question_id, Question.user_id == user.id)
results = session.exec(statement)
question = results.first()
return question return question
# def get_question_by_id(question_id: int):
# with Session(engine) as session:
# question = session.get(Question, question_id)
# return question
def get_questions(knowledge): def get_questions(knowledge):
with Session(engine) as session: with Session(engine) as session:
statement = select(Question).where(Question.knowledge_id == knowledge.id) statement = select(Question).where(Question.knowledge_id == knowledge.id)

View File

@@ -15,5 +15,4 @@ class Metric(SQLModel, table=True):
user: User | None = Relationship(back_populates="metrics") user: User | None = Relationship(back_populates="metrics")
class MetricCreate(BaseModel): class MetricCreate(BaseModel):
question_id: int
need_index: int need_index: int

View File

@@ -21,14 +21,4 @@ import AppTopbar from '@/components/AppTopbar.vue'
padding-inline: 5%; padding-inline: 5%;
height: 100vh; height: 100vh;
} }
/* main {
height: 100%;
}
@media screen and (min-width: 768px) {
#app {
height: 100vh;
}
} */
</style> </style>

View File

@@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy"
//import { apiClient } from "@/services/api"; import api from "@/services/api"
import api from "@/services/apiAxios" import type { AxiosResponse } from "axios"
import type { AxiosResponse } from "axios"; import { ref } from "vue"
import { ref } from "vue";
import type { Knowledge, KnowledgeCreate } from "@/types/types"; import type { Knowledge, KnowledgeCreate } from "@/types/types"
import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
@@ -45,8 +44,8 @@
<template> <template>
<div class="container"> <div class="container">
<h2>Collect Knowledge</h2> <h2>Ajouter une connaissance</h2>
<b-field label="Knowledge"> <b-field label="Connaissance">
<!-- @vue-ignore --> <!-- @vue-ignore -->
<b-input <b-input
v-model="knowledgeModel" v-model="knowledgeModel"
@@ -68,7 +67,7 @@
<div class="btn-container"> <div class="btn-container">
<b-field> <b-field>
<div class="control"> <div class="control">
<b-button type="is-primary" @click="postKnowledge" >Share</b-button> <b-button type="is-primary" @click="postKnowledge" >Partager</b-button>
</div> </div>
</b-field> </b-field>
</div> </div>

View File

@@ -1,35 +1,43 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { onBeforeMount, ref, watch } from "vue";
import { onBeforeMount } from 'vue'
import type { Knowledge, Question, MetricCreate } from "@/types/types" import type { Knowledge, Question, MetricCreate, Metric } from "@/types/types"
//import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
import api from "@/services/apiAxios" import api from "@/services/api"
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
//const stepStore = useStepStore() const stepStore = useStepStore()
const itemStore = useItemStore() const itemStore = useItemStore()
const questions = ref<Question[]>() const questions = ref<Question[]>()
const metrics = ref<MetricCreate[]>([]) const metrics = ref<Metric[]>([])
onBeforeMount(async () => { onBeforeMount(async () => {
if(itemStore.knowledge != undefined){ if(!itemStore.knowledge){
questions.value = await getQuestions(itemStore.knowledge)
}
else{
throw new Error("There is no knowledge element in itemStore."); throw new Error("There is no knowledge element in itemStore.");
} }
if(questions.value != undefined){ questions.value = await getQuestions(itemStore.knowledge)
initializeMetrics(questions.value)
} if(!questions.value){
else{
throw new Error("There is no questions element from API."); throw new Error("There is no questions element from API.");
} }
initializeMetrics(questions.value)
})
watch( () => itemStore.knowledge, async () =>{
metrics.value = []
if(!itemStore.knowledge){
throw new Error("There is no knowledge element in itemStore.");
}
questions.value = await getQuestions(itemStore.knowledge)
if(!questions.value){
throw new Error("There is no questions element from API.");
}
initializeMetrics(questions.value)
}) })
async function getQuestions(knowledge: Knowledge): Promise<Question[]>{ async function getQuestions(knowledge: Knowledge): Promise<Question[]>{
@@ -40,7 +48,7 @@
function initializeMetrics(questions: Question[]){ function initializeMetrics(questions: Question[]){
questions.forEach((q)=>{ questions.forEach((q)=>{
const metric: MetricCreate = { const metric: Metric = {
question_id: q.id!, question_id: q.id!,
need_index: -1 need_index: -1
} }
@@ -58,11 +66,14 @@
} }
async function postMetrics(){ async function postMetrics(){
console.log( metrics.value)
metrics.value?.forEach(async (metric) => { metrics.value?.forEach(async (metric) => {
const response = await api.post(`api/v1/metrics/`, metric) const metricCreate: MetricCreate = { need_index: metric.need_index }
const response = await api.post(`api/v1/questions/${metric.question_id}/metrics`, metricCreate)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const metric_data = response.data const metric_data = response.data
}) })
stepStore.nextStep()
} }
</script> </script>
@@ -80,7 +91,7 @@
<div class="btn-container"> <div class="btn-container">
<b-field> <b-field>
<div class="control"> <div class="control">
<b-button type="is-primary" @click="postMetrics" >Share</b-button> <b-button type="is-primary" @click="postMetrics" >Partager</b-button>
</div> </div>
</b-field> </b-field>
</div> </div>

View File

@@ -3,10 +3,12 @@
import { BProgress } from "buefy"; import { BProgress } from "buefy";
import type { Knowledge } from "@/types/types"; import type { Knowledge } from "@/types/types";
import api from "@/services/apiAxios"; import api from "@/services/api";
import { useItemStore } from '@/stores/item' import { useItemStore } from '@/stores/item'
import { useStepStore } from '@/stores/step' import { useStepStore } from '@/stores/step'
import { ProcessStep } from '@/services/knowledgeWorklow'
const stepStore = useStepStore() const stepStore = useStepStore()
const itemStore = useItemStore() const itemStore = useItemStore()
@@ -18,6 +20,7 @@
async function generateQuestions (knowledge: Knowledge) { async function generateQuestions (knowledge: Knowledge) {
await api.post(`api/v1/knowledges/${knowledge.id}/questions`) await api.post(`api/v1/knowledges/${knowledge.id}/questions`)
if(stepStore.indexStep == ProcessStep.WaitGeneration)
stepStore.nextStep() stepStore.nextStep()
} }
</script> </script>

View File

@@ -1,10 +1,116 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, onBeforeMount } from "vue"
import api from "@/services/api"
import { BButton } from "buefy"
import { identifyProcessStep, ProcessStep }from "@/services/knowledgeWorklow"
import type { AxiosResponse } from "axios"
import type { Knowledge } from "@/types/types"
import { useStepStore } from '@/stores/step'
import { useItemStore } from '@/stores/item'
const stepStore = useStepStore()
const itemStore = useItemStore()
interface KnowledgesWorkflow {
knowledge: Knowledge,
processStep: ProcessStep,
isSelected: boolean
}
const knowledgesWorkflow = reactive<KnowledgesWorkflow[]>([])
async function getKnowledges(): Promise<Knowledge[]>{
const response: AxiosResponse<Knowledge[]> = await api.get("api/v1/knowledges/")
return response.data
}
function truncateString(str: string){
return (str.length <= 18) ? str : str.slice(0, 9) + "..." + str.slice(-9)
}
async function initializeKnowledgeWorkflow(){
const knowledges: Knowledge[] = await getKnowledges()
knowledges.forEach(async (knowledge) => {
const kWorkflow: KnowledgesWorkflow = {
knowledge,
processStep: await identifyProcessStep(knowledge as Knowledge),
isSelected: false
}
knowledgesWorkflow.push(kWorkflow)
})
}
async function getKnowledgeWorkflow(){
if(knowledgesWorkflow.length == 0){
initializeKnowledgeWorkflow()
} else {
const knowledges: Knowledge[] = await getKnowledges()
knowledges.forEach(async (knowledge) => {
const kWorkflow: KnowledgesWorkflow = {
knowledge,
processStep: await identifyProcessStep(knowledge as Knowledge),
isSelected: false
}
const indexKW = knowledgesWorkflow.findIndex((kW: KnowledgesWorkflow) => kW.knowledge.id === knowledge.id)
indexKW != -1 ? knowledgesWorkflow[indexKW] = kWorkflow : knowledgesWorkflow.push(kWorkflow)
})
}
}
onBeforeMount(() => getKnowledgeWorkflow())
onMounted(()=>{
setInterval(() => getKnowledgeWorkflow(), 10000)
})
function goToEvaluateQuestion(knowledge_data: Knowledge){
itemStore.$patch({ knowledge: knowledge_data })
stepStore.goToStep(ProcessStep.EvaluateQuestion)
}
function goToCollectKnowledge(){
stepStore.goToStep(ProcessStep.CollectKnowledge)
}
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<h2>Knowledge</h2> <h2>Connaissances</h2>
<ul>
<li v-for="(kW, index) in knowledgesWorkflow" :key="index" class="list-element">
<div class="list-element-index">
{{ index + 1 }}
</div>
<div class="list-element-body">
<span class="list-element-text">
{{ truncateString(kW.knowledge.uri) }}
</span>
<!-- <b-button v-if="kW.processStep == ProcessStep.CollectKnowledge" type="is-primary" @click="goTo" >
Collect
</b-button> -->
<b-button v-if="kW.processStep == ProcessStep.WaitGeneration" type="is-info is-light" loading >
...
</b-button>
<b-button v-if="kW.processStep == ProcessStep.EvaluateQuestion" type="is-warning" @click="goToEvaluateQuestion(kW.knowledge)" >
Evaluer
</b-button>
<b-button v-if="kW.processStep == ProcessStep.ProcessDone" type="is-success is-light" disabled >
Fait
</b-button>
</div>
</li>
<li >
<b-button type="is-primary" outlined @click="goToCollectKnowledge()" >
Ajouter une connaissance
</b-button>
</li>
</ul>
</div> </div>
</template> </template>
@@ -14,4 +120,29 @@
border-radius: 16px; border-radius: 16px;
padding: 34px; padding: 34px;
} }
.list-element{
margin-bottom: 8px;
border: 1px solid #D6D9E0;
border-radius: 8px;
display: grid;
grid-template-columns: 40px 1fr;
}
.list-element-index{
display: flex;
justify-content: center;
align-items: center;
border-right: 1px solid #D6D9E0;
}
.list-element-body{
display: grid;
grid-template-columns: 66% 34%;
}
.list-element-text{
display: flex;
align-items: center;
padding-left: 12px;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { isAuthenticated } from '@/services/apiAxios' import { isAuthenticated } from '@/services/api'
const pagesWithoutGuard = ['login', 'app', 'register'] const pagesWithoutGuard = ['login', 'app', 'register']
@@ -33,9 +33,10 @@ const router = createRouter({
], ],
}) })
// Guard system
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
const isAuth = await isAuthenticated() const isAuth = await isAuthenticated()
if (!isAuth && pagesWithoutGuard.includes(to.name!.toString())) { if (!isAuth && !pagesWithoutGuard.includes(to.name!.toString())) {
return { name: 'login' } return { name: 'login' }
} }
}) })

View File

@@ -1,74 +1,45 @@
class ApiClient { import axios from "axios"
private baseURL: string import type { AxiosResponse } from "axios";
private defaultHeaders: Record<string, string>
constructor(baseURL: string) { const api = axios.create({
this.baseURL = baseURL baseURL: import.meta.env.VITE_API_URL
this.defaultHeaders = { });
'Content-Type': 'application/json',
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token){
config.headers.Authorization = `Bearer ${token}`
} }
return config
})
export const authAPI = {
register: (username: string, password: string) =>
api.post(
'/api/v1/auth/register',
{ "username":username, "plain_password":password }
),
login: (username: string, password: string) =>
api.post(
'/api/v1/auth/login',
new URLSearchParams({ username, password }),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
),
getMe: () => api.get('/api/v1/auth/me')
} }
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { export const isAuthenticated = async () => {
const url = `${this.baseURL}${endpoint}`
const config: RequestInit = {
headers: { ...this.defaultHeaders, ...options.headers },
...options,
}
let response
try { try {
response = await fetch(url, config) const response: AxiosResponse = await authAPI.getMe()
} catch (error) { if (response.status==200)
if (error instanceof Error) { return true
throw new HTTPError(`Network error: ${error.message}`) else
return false
} }
throw new HTTPError('Unknown network error') catch{
return false
} }
if (response?.ok) {
return response.json()
} }
const errorData = await response.json().catch(() => ({})) export default api;
throw new HTTPError(
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
response.status,
response,
)
}
/**
* HTTP Get
* @param endpoint
* @returns
*/
async get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'GET' })
}
/**
* HTTP Post
* @param endpoint
* @param data
* @returns
*/
async post<T>(endpoint: string, data: unknown): Promise<T> {
return this.request<T>(endpoint, { method: 'POST', body: JSON.stringify(data) })
}
}
class HTTPError extends Error {
constructor(
message: string,
private status: number | null = null,
private reponse: Response | null = null,
) {
super(message)
this.name = 'HTTPError'
}
}
export const apiClient = new ApiClient(import.meta.env.VITE_API_URL)

View File

@@ -1,45 +0,0 @@
import axios from "axios"
import type { AxiosResponse } from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token){
config.headers.Authorization = `Bearer ${token}`
}
return config
})
export const authAPI = {
register: (username: string, password: string) =>
api.post(
'/api/v1/auth/register',
{ "username":username, "plain_password":password }
),
login: (username: string, password: string) =>
api.post(
'/api/v1/auth/login',
new URLSearchParams({ username, password }),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}
),
getMe: () => api.get('/api/v1/auth/me')
}
export const isAuthenticated = async () => {
try {
const response: AxiosResponse = await authAPI.getMe()
if (response.status==200)
return true
else
return false
}
catch{
return false
}
}
export default api;

View File

@@ -0,0 +1,54 @@
import type { Knowledge, Metric, Question } from "@/types/types"
import api from "@/services/api"
import type { AxiosResponse } from "axios"
const processFlow = ["CollectKnowledge", "GenerateQuestion", "EvaluateQuestion", "Done"]
//From user action in worlfow
export enum ProcessStep {
CollectKnowledge,
WaitGeneration,
EvaluateQuestion,
ProcessDone
}
export async function identifyProcessStep(knowledge: Knowledge | null): Promise<ProcessStep>{
if (knowledge == null)
return ProcessStep.CollectKnowledge
//Check questions exist
const apiGetQuestions: AxiosResponse<Question[]> = await api.get(
`api/v1/knowledges/${knowledge.id}/questions/`,
{
validateStatus: function (status) {
return status < 500
},
})
if (apiGetQuestions.status >= 400 && apiGetQuestions.status < 500)
return ProcessStep.WaitGeneration
const questions: Question[] = apiGetQuestions.data
if (questions.length == 0)
return ProcessStep.WaitGeneration
//Check metrics exist
const apiGetMetrics: AxiosResponse<Metric[]> = await api.get(
`api/v1/questions/${questions[0]!.id}/metrics/`,
{
validateStatus: function (status) {
return status < 500
},
})
if (apiGetMetrics.status >= 400 && apiGetMetrics.status < 500)
return ProcessStep.EvaluateQuestion
const metrics: Metric[] = apiGetMetrics.data
if (metrics.length == 0)
return ProcessStep.EvaluateQuestion
//Data is complete
return ProcessStep.ProcessDone
}

View File

@@ -5,6 +5,8 @@ import CollectKnowledge from '@/components/CollectKnowledge.vue'
import EvaluateQuestion from '@/components/EvaluateQuestion.vue' import EvaluateQuestion from '@/components/EvaluateQuestion.vue'
import GenerateQuestion from '@/components/GenerateQuestion.vue' import GenerateQuestion from '@/components/GenerateQuestion.vue'
import { ProcessStep } from '@/services/knowledgeWorklow'
const steps: Component = [ const steps: Component = [
CollectKnowledge, CollectKnowledge,
GenerateQuestion, GenerateQuestion,
@@ -17,8 +19,15 @@ export const useStepStore = defineStore('step', () => {
const getCurrentComponent = computed(() => steps[indexStep.value]) const getCurrentComponent = computed(() => steps[indexStep.value])
function nextStep() { function nextStep() {
if(indexStep.value + 1 < steps.length)
indexStep.value++ indexStep.value++
else
indexStep.value = 0
} }
return { steps, getCurrentComponent, nextStep } function goToStep(processStep: ProcessStep){
indexStep.value = processStep
}
return { indexStep, steps, getCurrentComponent, nextStep, goToStep }
}) })

View File

@@ -18,9 +18,15 @@ interface Question {
user: User user: User
} }
interface MetricCreate { interface Metric {
//id
question_id: number, question_id: number,
need_index: number need_index: number
//user
}
interface MetricCreate {
need_index: number
} }
interface User { interface User {
@@ -28,4 +34,4 @@ interface User {
token: string token: string
} }
export type {KnowledgeCreate, Knowledge, Question, MetricCreate} export type {KnowledgeCreate, Knowledge, Question, MetricCreate, Metric}

View File

@@ -2,7 +2,7 @@
import router from '@/router/index' import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue" import { ref } from "vue"
import { authAPI } from '@/services/apiAxios' import { authAPI } from '@/services/api'
const username = ref<string>("") const username = ref<string>("")
const password = ref<string>("") const password = ref<string>("")

View File

@@ -2,7 +2,7 @@
import router from '@/router/index' import router from '@/router/index'
import { BField, BInput, BButton, useToast } from "buefy"; import { BField, BInput, BButton, useToast } from "buefy";
import { ref } from "vue" import { ref } from "vue"
import { authAPI } from '@/services/apiAxios' import { authAPI } from '@/services/api'
const username = ref<string>("") const username = ref<string>("")
const password = ref<string>("") const password = ref<string>("")