Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions desdeo/api/models/generic_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

from desdeo.problem import Tensor, VariableType

from .nautilus import (
NautilusState,
)
from .state import (
EMOFetchState,
EMOIterateState,
Expand Down Expand Up @@ -64,6 +67,8 @@ class StateKind(str, Enum):
GENERIC_INTERMEDIATE = "generic.solve_intermediate"
ENAUTILUS_STEP = "e-nautilus.stepping"
ENAUTILUS_FINAL = "e-nautilus.final"
NAUTILUS_NAVIGATE = "nautilus.navigate"
NAUTILUS_INITIALIZE = "nautilus.initialize"


class State(SQLModel, table=True):
Expand Down Expand Up @@ -155,6 +160,63 @@ def create(

return row

# @classmethod
# def create(
# cls,
# database_session: Session,
# *,
# kind: StateKind,
# problem_id: int | None = None,
# session_id: int | None = None,
# parent_id: int | None = None,
# state: SQLModel | None = None,
# ) -> "StateDB":
# """Create a new StateDB entry and corresponding substate entry.

# Args:
# database_session: Database session
# kind: Explicit StateKind of this state.
# problem_id: Required for root states.
# session_id: Required for child states.
# parent_id: Parent state ID if this is a child.
# state: The substate SQLModel instance.
# """
# if state is None:
# raise ValueError("State (substate) must be provided.")

# if parent_id is None:
# # Root state
# if problem_id is None:
# raise ValueError("Root state requires problem_id.")
# state_db = cls(
# problem_id=problem_id,
# session_id=None,
# parent_id=None,
# kind=kind,
# )
# else:
# # Child state
# if session_id is None:
# raise ValueError("Child state requires session_id.")
# state_db = cls(
# problem_id=None,
# session_id=session_id,
# parent_id=parent_id,
# kind=kind,
# )

# database_session.add(state_db)
# database_session.commit()
# database_session.refresh(state_db)

# # Link substate
# state.state_id = state_db.id
# database_session.add(state)
# database_session.commit()
# database_session.refresh(state_db)

# return state_db

@property
def state(self) -> SQLModel | None:
"""Return the concrete substate instance (e.g., NIMBUSSaveState)...
Expand Down Expand Up @@ -194,6 +256,8 @@ def state(self) -> SQLModel | None:
StateKind.GENERIC_INTERMEDIATE: IntermediateSolutionState,
StateKind.ENAUTILUS_STEP: ENautilusState,
StateKind.ENAUTILUS_FINAL: ENautilusFinalState,
StateKind.NAUTILUS_NAVIGATE: NautilusState,
StateKind.NAUTILUS_INITIALIZE: NautilusState
}

SUBSTATE_TO_KIND: dict[SQLModel, StateKind] = {
Expand All @@ -212,6 +276,8 @@ def state(self) -> SQLModel | None:
IntermediateSolutionState: StateKind.GENERIC_INTERMEDIATE,
ENautilusState: StateKind.ENAUTILUS_STEP,
ENautilusFinalState: StateKind.ENAUTILUS_FINAL,
# NautilusState: StateKind.NAUTILUS_NAVIGATE,
# NautilusState: StateKind.NAUTILUS_INITIALIZE
}


Expand Down
79 changes: 49 additions & 30 deletions desdeo/api/models/nautilus.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
import datetime
from sqlmodel import JSON, Column, Field, SQLModel

from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String

from desdeo.api.db import Base
class NautilusState(SQLModel, table=True):
"""Concrete NAUTILUS Navigator state stored for a single interaction step.

This model stores the full algorithmic state returned by the
NAUTILUS Navigator after either initialization or a navigation step.

class NautilusStateDB(Base):
"""Database model storing the state of a NAUTILUS interactive navigation session.

Each record represents a single interaction step within a NAUTILUS session,
such as initialization, navigation, or finalization. The table supports
branching session trees by maintaining parent-child relationships between states.
The instance is linked to a base `StateDB` entry, which defines the
problem context and state type (e.g. "nautilus.initialize" or
"nautilus.navigate"). This table contains only the algorithm-specific
data required to reconstruct the navigation process at that step.

Attributes:
id (int): Primary key identifying this state entry.
session_id (int): Identifier of the interactive NAUTILUS session.
parent_state_id (int | None): Foreign key referencing the parent state.
Null for root (initialization) nodes.
request (JSON): Serialized Nautilus request model used to generate this state.
response (JSON): Serialized Nautilus response model produced by the algorithm.
node_type (str): Type of interaction that generated the state.
Expected values include:
- "initialize"
- "navigate"
- "final"
depth (int): Depth of this node within the session tree (root = 0).
created_at (datetime): Timestamp when this state was created.
id (int | None): Primary key of this NAUTILUS state entry.
objective_symbols (list[str]): Short symbolic names of the objectives.
objective_long_names (list[str]): Descriptive names of the objectives.
units (list[str] | None): Units of the objectives, if defined. None if unitless.
is_maximized (list[bool]): Boolean flags indicating whether each objective
is to be maximized (True) or minimized (False).
ideal (list[float]): Ideal objective values of the problem.
nadir (list[float]): Nadir objective values of the problem.
lower_bounds (dict[str, list[float]]): Lower bounds of the reachable region per objective
across navigation steps.
upper_bounds (dict[str, list[float]]): Upper bounds of the reachable region per objective
across navigation steps.
preferences (dict[str, list[float]]): Preference values provided by the decision maker
for each navigation step.
bounds (dict[str, list[float]]): Bound preferences provided by the decision maker
for each navigation step.
total_steps (int): Total number of steps allowed in this NAUTILUS session.
current_step (int): Current navigation step index.
remaining_steps (int): Number of steps remaining in the navigation process.
reachable_solution (dict[str, float]): The objective values of the currently reachable solution.
"""

__tablename__ = "nautilus_states"

id = Column(Integer, primary_key=True)
session_id = Column(Integer, nullable=False)
parent_state_id = Column(Integer, ForeignKey("nautilus_states.id"), nullable=True)
id: int | None = Field(default=None, primary_key=True)

# Problem meta
objective_symbols: list[str] = Field(sa_column=Column(JSON))
objective_long_names: list[str] = Field(sa_column=Column(JSON))
units: list[str] | None = Field(default=None, sa_column=Column(JSON))
is_maximized: list[bool] = Field(sa_column=Column(JSON))

request = Column(JSON, nullable=False)
response = Column(JSON, nullable=False)
# Problem bounds
ideal: list[float] = Field(sa_column=Column(JSON))
nadir: list[float] = Field(sa_column=Column(JSON))

node_type = Column(String, nullable=False) # "initialize", "navigate", "final"
# Navigation data
lower_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON))
upper_bounds: dict[str, list[float]] = Field(sa_column=Column(JSON))
preferences: dict[str, list[float]] = Field(sa_column=Column(JSON))
bounds: dict[str, list[float]] = Field(sa_column=Column(JSON))

depth = Column(Integer, nullable=False)
total_steps: int
current_step: int
remaining_steps: int

created_at = Column(DateTime, default=datetime.utcnow)
# Final reachable solution
reachable_solution: dict[str, float] = Field(sa_column=Column(JSON))
4 changes: 1 addition & 3 deletions desdeo/api/schemas/nautilus.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class NautilusInitialResponse(BaseModel):

state_id: int = Field(..., description="The ID of this navigation state.")
response_type: Literal["nautilus.initialize"] = "nautilus.initialize"
node_type: Literal["initialize"] = "initialize"
parent_state_id: int | None = Field(None, description="Parent state ID, if this is a child step.")

objective_symbols: list[str] = Field(..., description="The symbols of the objectives.")
Expand All @@ -42,7 +41,6 @@ class NautilusNavigateResponse(BaseModel):

state_id: int = Field(..., description="The ID of this navigation state.")
response_type: Literal["nautilus.navigate"] = "nautilus.navigate"
node_type: Literal["navigate", "final"] = "navigate"
parent_state_id: int | None = Field(None, description="Parent state ID, if this is a child step.")

objective_symbols: list[str] = Field(..., description="The symbols of the objectives.")
Expand All @@ -58,4 +56,4 @@ class NautilusNavigateResponse(BaseModel):
bounds: dict[str, list[float]] = Field(..., description="Bounds used in each step per objective.")

total_steps: int = Field(..., description="The total number of steps in the current navigation path.")
reachable_solution: dict = Field(..., description="The solution reached at the end of navigation.")
reachable_solution: dict[str, float] = Field(..., description="The solution reached at the end of navigation.")
11 changes: 11 additions & 0 deletions webui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ ncu -u

this will upgrade the project's packages to their latest (mutually) compatible versions. **This can introduce breaking changes!**

## WSL / HTTP note

Auth cookies are set with `secure: !dev`, meaning they require HTTPS only in
production builds. In development mode (`npm run dev`), cookies are sent over
plain HTTP, which is necessary when running through WSL2 on Windows where the
browser may not treat the forwarded address as a secure context.

If you build and preview a production bundle locally over HTTP, authentication
will fail because `secure` cookies are not sent. Use `npm run dev` for local
development, or serve the production build behind HTTPS.

## Developing

Once the project's dependencies have been installed, start a development server:
Expand Down
3 changes: 2 additions & 1 deletion webui/src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { HandleFetch } from '@sveltejs/kit';
import { refreshAccessTokenRefreshPost } from '$lib/gen/endpoints/DESDEOFastAPI';
import { dev } from '$app/environment';

// const API = process.env.API_BASE_URL ?? '/';

Expand Down Expand Up @@ -29,7 +30,7 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
// access ok!
event.cookies.set("access_token", response_with_new_cookies.data.access_token, {
httpOnly: true,
secure: true,
secure: !dev,
sameSite: "lax",
path: "/",
});
Expand Down
2 changes: 1 addition & 1 deletion webui/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { get } from 'svelte/store';
import { browser } from '$app/environment';


const BASE_URL = import.meta.env.VITE_API_URL;
const BASE_URL = (import.meta.env.VITE_API_URL as string).replace(/\/+$/, '');

export const api = createClient<paths>({baseUrl: BASE_URL});

Expand Down
5 changes: 3 additions & 2 deletions webui/src/routes/home/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { zod4 } from "sveltekit-superforms/adapters";
import { loginLoginPost } from "$lib/gen/endpoints/DESDEOFastAPI";
import type { BodyLoginLoginPost } from '$lib/gen/models';
import { redirect, type Actions } from "@sveltejs/kit";
import { dev } from "$app/environment";

const loginSchema = z.object({
username: z.string(),
Expand Down Expand Up @@ -37,8 +38,8 @@ export const actions: Actions = {
return fail(response.status);
}

cookies.set("access_token", response.data.access_token, {httpOnly: true, secure: true, sameSite: "lax", path: '/'});
cookies.set("refresh_token", response.data.refresh_token, {httpOnly: true, secure: true, sameSite: "lax", path: '/'});
cookies.set("access_token", response.data.access_token, {httpOnly: true, secure: !dev, sameSite: "lax", path: '/'});
cookies.set("refresh_token", response.data.refresh_token, {httpOnly: true, secure: !dev, sameSite: "lax", path: '/'});

redirect(303, '/dashboard');
},
Expand Down
Loading