Implement session management and model discovery APIs
This commit is contained in:
parent
eed9d1da66
commit
b30a7e4770
65
pom.xml
65
pom.xml
@ -19,6 +19,7 @@
|
|||||||
<lombok.version>1.18.42</lombok.version>
|
<lombok.version>1.18.42</lombok.version>
|
||||||
<junit.jupiter.version>5.10.3</junit.jupiter.version>
|
<junit.jupiter.version>5.10.3</junit.jupiter.version>
|
||||||
<mockito.version>5.12.0</mockito.version>
|
<mockito.version>5.12.0</mockito.version>
|
||||||
|
<openapi.generator.version>7.11.0</openapi.generator.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@ -39,6 +40,18 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-arc</artifactId>
|
<artifactId>quarkus-arc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rest</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rest-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-liquibase</artifactId>
|
<artifactId>quarkus-liquibase</artifactId>
|
||||||
@ -76,6 +89,58 @@
|
|||||||
<version>${quarkus.plugin.version}</version>
|
<version>${quarkus.plugin.version}</version>
|
||||||
<extensions>true</extensions>
|
<extensions>true</extensions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>build-helper-maven-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>add-openapi-generated-sources</id>
|
||||||
|
<phase>generate-sources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>add-source</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<sources>
|
||||||
|
<source>${project.build.directory}/generated-sources/openapi/src/main/java</source>
|
||||||
|
</sources>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openapitools</groupId>
|
||||||
|
<artifactId>openapi-generator-maven-plugin</artifactId>
|
||||||
|
<version>${openapi.generator.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>generate-roleplay-api</id>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<inputSpec>${project.basedir}/src/main/resources/openapi-roleplay-public-v1.yml</inputSpec>
|
||||||
|
<generatorName>jaxrs-spec</generatorName>
|
||||||
|
<output>${project.build.directory}/generated-sources/openapi</output>
|
||||||
|
<apiPackage>de.neitzel.roleplay.fascade.api</apiPackage>
|
||||||
|
<modelPackage>de.neitzel.roleplay.fascade.model</modelPackage>
|
||||||
|
<configOptions>
|
||||||
|
<interfaceOnly>true</interfaceOnly>
|
||||||
|
<returnResponse>false</returnResponse>
|
||||||
|
<useSwaggerAnnotations>false</useSwaggerAnnotations>
|
||||||
|
<useBeanValidation>true</useBeanValidation>
|
||||||
|
<sourceFolder>src/main/java</sourceFolder>
|
||||||
|
<dateLibrary>java8</dateLibrary>
|
||||||
|
<openApiNullable>false</openApiNullable>
|
||||||
|
<useTags>true</useTags>
|
||||||
|
<jackson>true</jackson>
|
||||||
|
<useJakartaEe>true</useJakartaEe>
|
||||||
|
</configOptions>
|
||||||
|
<generateSupportingFiles>false</generateSupportingFiles>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>${maven.compiler.plugin.version}</version>
|
<version>${maven.compiler.plugin.version}</version>
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
package de.neitzel.roleplay.business;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.SessionResponse;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnResponse;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory implementation of {@link SessionService}. Stores sessions in a
|
||||||
|
* {@link ConcurrentHashMap}; suitable for development and testing. A
|
||||||
|
* production implementation would persist state in a database.
|
||||||
|
*
|
||||||
|
* <p>Turn orchestration (the two-call Ollama pattern) is not yet wired; the
|
||||||
|
* methods return stub responses so the REST layer can be exercised end-to-end.
|
||||||
|
* The {@code TODO} markers indicate where the Ollama integration must be added.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class InMemorySessionService implements SessionService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory store mapping session IDs to their current state.
|
||||||
|
*/
|
||||||
|
private final Map<String, SessionResponse> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Generates a new UUID as the session ID, populates default session
|
||||||
|
* metadata, and stores the session. The Ollama two-call pattern is not
|
||||||
|
* yet invoked; a placeholder narrative is returned instead.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SessionResponse createSession(final CreateSessionRequest request) {
|
||||||
|
String sessionId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
SessionResponse session = new SessionResponse(
|
||||||
|
sessionId,
|
||||||
|
request.getModel(),
|
||||||
|
request.getLanguage() != null ? request.getLanguage() : "en",
|
||||||
|
request.getSafetyLevel() != null
|
||||||
|
? request.getSafetyLevel().value()
|
||||||
|
: CreateSessionRequest.SafetyLevelEnum.STANDARD.value(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Invoke OllamaClient two-call pattern (narrative + state extraction)
|
||||||
|
// to produce a real opening scene and initial state.
|
||||||
|
session.setNarrative("Session initialised. Ollama integration pending.");
|
||||||
|
|
||||||
|
sessions.put(sessionId, session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<SessionResponse> getSession(final String sessionId) {
|
||||||
|
return Optional.ofNullable(sessions.get(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Increments the turn counter and returns a stub {@link TurnResponse}.
|
||||||
|
* The Ollama two-call pattern is not yet invoked.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<TurnResponse> submitTurn(final String sessionId,
|
||||||
|
final TurnRequest turnRequest) {
|
||||||
|
SessionResponse session = sessions.get(sessionId);
|
||||||
|
if (session == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment turn counter
|
||||||
|
int nextTurn = session.getTurnNumber() + 1;
|
||||||
|
session.setTurnNumber(nextTurn);
|
||||||
|
|
||||||
|
// TODO: Invoke OllamaClient two-call pattern (narrative + state update)
|
||||||
|
// using the current session state, turnRequest.getUserAction(),
|
||||||
|
// and turnRequest.getRecommendation().
|
||||||
|
TurnResponse response = new TurnResponse(
|
||||||
|
nextTurn,
|
||||||
|
"Turn " + nextTurn + " processed. Ollama integration pending."
|
||||||
|
);
|
||||||
|
|
||||||
|
sessions.put(sessionId, session);
|
||||||
|
return Optional.of(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
90
src/main/java/de/neitzel/roleplay/business/ModelService.java
Normal file
90
src/main/java/de/neitzel/roleplay/business/ModelService.java
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package de.neitzel.roleplay.business;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.OllamaClient;
|
||||||
|
import de.neitzel.roleplay.fascade.OllamaModelInfo;
|
||||||
|
import de.neitzel.roleplay.fascade.model.ModelInfo;
|
||||||
|
import de.neitzel.roleplay.fascade.model.ModelListResponse;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business service responsible for discovering available Ollama AI models and
|
||||||
|
* converting them into public API representations.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ModelService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ollama facade used to query the installed model list.
|
||||||
|
*/
|
||||||
|
private final OllamaClient ollamaClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ModelService}.
|
||||||
|
*
|
||||||
|
* @param ollamaClient the Ollama client facade
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public ModelService(final OllamaClient ollamaClient) {
|
||||||
|
this.ollamaClient = ollamaClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all models currently installed on the Ollama server, mapped to
|
||||||
|
* the public {@link ModelListResponse} API schema.
|
||||||
|
*
|
||||||
|
* @return list response containing zero or more {@link ModelInfo} entries
|
||||||
|
*/
|
||||||
|
public ModelListResponse listModels() {
|
||||||
|
List<OllamaModelInfo> ollamaModels = ollamaClient.listModels();
|
||||||
|
List<ModelInfo> apiModels = ollamaModels.stream()
|
||||||
|
.map(this::toApiModel)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
ModelListResponse response = new ModelListResponse();
|
||||||
|
response.setModels(apiModels);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a single Ollama model info to the public API {@link ModelInfo} schema.
|
||||||
|
*
|
||||||
|
* @param ollamaModelInfo the source Ollama model metadata
|
||||||
|
* @return the mapped API model info
|
||||||
|
*/
|
||||||
|
private ModelInfo toApiModel(final OllamaModelInfo ollamaModelInfo) {
|
||||||
|
ModelInfo info = new ModelInfo(ollamaModelInfo.getName());
|
||||||
|
info.setSize(ollamaModelInfo.getSize());
|
||||||
|
if (ollamaModelInfo.getDetails() != null) {
|
||||||
|
info.setFamily(ollamaModelInfo.getDetails().getFamily());
|
||||||
|
}
|
||||||
|
// Derive a human-readable display name from the technical name
|
||||||
|
String displayName = deriveDisplayName(ollamaModelInfo.getName());
|
||||||
|
info.setDisplayName(displayName);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a human-readable display name from a technical model identifier.
|
||||||
|
* For example {@code "llama3:latest"} becomes {@code "Llama3 (latest)"}.
|
||||||
|
*
|
||||||
|
* @param technicalName the raw model name including optional tag
|
||||||
|
* @return a human-readable display name
|
||||||
|
*/
|
||||||
|
private String deriveDisplayName(final String technicalName) {
|
||||||
|
if (technicalName == null || technicalName.isBlank()) {
|
||||||
|
return technicalName;
|
||||||
|
}
|
||||||
|
String[] parts = technicalName.split(":", 2);
|
||||||
|
String baseName = parts[0];
|
||||||
|
// Capitalise first letter
|
||||||
|
String capitalised = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1);
|
||||||
|
if (parts.length == 2) {
|
||||||
|
return capitalised + " (" + parts[1] + ")";
|
||||||
|
}
|
||||||
|
return capitalised;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package de.neitzel.roleplay.business;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.SessionResponse;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnResponse;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the contract for managing role-play sessions. Implementations are
|
||||||
|
* responsible for session lifecycle (creation, retrieval, turn processing) and
|
||||||
|
* state persistence.
|
||||||
|
*/
|
||||||
|
public interface SessionService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new role-play session based on the provided request. Runs the
|
||||||
|
* two-call Ollama pattern to produce an opening narrative and initial state.
|
||||||
|
*
|
||||||
|
* @param request the session creation parameters
|
||||||
|
* @return the full initial session state including opening narrative and suggestions
|
||||||
|
*/
|
||||||
|
SessionResponse createSession(CreateSessionRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current state of an existing session.
|
||||||
|
*
|
||||||
|
* @param sessionId the unique session identifier
|
||||||
|
* @return an {@link Optional} containing the session response, or empty if not found
|
||||||
|
*/
|
||||||
|
Optional<SessionResponse> getSession(String sessionId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a user's turn within an existing session. Runs the two-call
|
||||||
|
* Ollama pattern and returns the resulting narrative with updated state.
|
||||||
|
*
|
||||||
|
* @param sessionId the unique session identifier
|
||||||
|
* @param turnRequest the user action and optional recommendation
|
||||||
|
* @return an {@link Optional} containing the turn response, or empty if session not found
|
||||||
|
*/
|
||||||
|
Optional<TurnResponse> submitTurn(String sessionId, TurnRequest turnRequest);
|
||||||
|
}
|
||||||
|
|
||||||
43
src/main/java/de/neitzel/roleplay/fascade/ModelResource.java
Normal file
43
src/main/java/de/neitzel/roleplay/fascade/ModelResource.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package de.neitzel.roleplay.fascade;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.business.ModelService;
|
||||||
|
import de.neitzel.roleplay.fascade.api.ModelsApi;
|
||||||
|
import de.neitzel.roleplay.fascade.model.ModelListResponse;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAX-RS resource that implements the {@link ModelsApi} interface generated
|
||||||
|
* from the OpenAPI specification. Delegates all business logic to
|
||||||
|
* {@link ModelService}.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ModelResource implements ModelsApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business service providing model discovery logic.
|
||||||
|
*/
|
||||||
|
private final ModelService modelService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code ModelResource}.
|
||||||
|
*
|
||||||
|
* @param modelService the model discovery service
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public ModelResource(final ModelService modelService) {
|
||||||
|
this.modelService = modelService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Delegates to {@link ModelService#listModels()} and returns the result
|
||||||
|
* directly.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ModelListResponse listModels() {
|
||||||
|
return modelService.listModels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package de.neitzel.roleplay.fascade;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.model.ErrorResponse;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAX-RS {@link ExceptionMapper} that converts a {@link NotFoundException}
|
||||||
|
* into a well-formed JSON {@link ErrorResponse} with HTTP status 404.
|
||||||
|
*/
|
||||||
|
@Provider
|
||||||
|
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a {@link NotFoundException} to a 404 response containing a
|
||||||
|
* standardised {@link ErrorResponse} body.
|
||||||
|
*
|
||||||
|
* @param exception the not-found exception thrown by a resource method
|
||||||
|
* @return a 404 response with a JSON error body
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Response toResponse(final NotFoundException exception) {
|
||||||
|
ErrorResponse error = new ErrorResponse("NOT_FOUND", exception.getMessage());
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.entity(error)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package de.neitzel.roleplay.fascade;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.business.SessionService;
|
||||||
|
import de.neitzel.roleplay.fascade.api.SessionsApi;
|
||||||
|
import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.SessionResponse;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAX-RS resource that implements the {@link SessionsApi} interface generated
|
||||||
|
* from the OpenAPI specification. Delegates all business logic to
|
||||||
|
* {@link SessionService}.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class SessionResource implements SessionsApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business service providing session lifecycle management.
|
||||||
|
*/
|
||||||
|
private final SessionService sessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code SessionResource}.
|
||||||
|
*
|
||||||
|
* @param sessionService the session management service
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public SessionResource(final SessionService sessionService) {
|
||||||
|
this.sessionService = sessionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Delegates to {@link SessionService#createSession(CreateSessionRequest)}.
|
||||||
|
* The HTTP 201 status is set by annotating with {@link jakarta.ws.rs.core.Response}
|
||||||
|
* in the generated interface; the resource simply returns the body.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SessionResponse createSession(final CreateSessionRequest createSessionRequest) {
|
||||||
|
return sessionService.createSession(createSessionRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Returns 404 if the session is not found.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SessionResponse getSession(final String sessionId) {
|
||||||
|
return sessionService.getSession(sessionId)
|
||||||
|
.orElseThrow(() -> new NotFoundException(
|
||||||
|
"No session found with id: " + sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
46
src/main/java/de/neitzel/roleplay/fascade/TurnResource.java
Normal file
46
src/main/java/de/neitzel/roleplay/fascade/TurnResource.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package de.neitzel.roleplay.fascade;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.business.SessionService;
|
||||||
|
import de.neitzel.roleplay.fascade.api.TurnsApi;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnResponse;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.NotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAX-RS resource that implements the {@link TurnsApi} interface generated
|
||||||
|
* from the OpenAPI specification. Delegates all business logic to
|
||||||
|
* {@link SessionService}.
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class TurnResource implements TurnsApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Business service providing turn submission and processing.
|
||||||
|
*/
|
||||||
|
private final SessionService sessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code TurnResource}.
|
||||||
|
*
|
||||||
|
* @param sessionService the session management service
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
public TurnResource(final SessionService sessionService) {
|
||||||
|
this.sessionService = sessionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* <p>Returns 404 if the session is not found.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public TurnResponse submitTurn(final String sessionId, final TurnRequest turnRequest) {
|
||||||
|
return sessionService.submitTurn(sessionId, turnRequest)
|
||||||
|
.orElseThrow(() -> new NotFoundException(
|
||||||
|
"No session found with id: " + sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,4 +9,5 @@ quarkus:
|
|||||||
url: http://debian:11434
|
url: http://debian:11434
|
||||||
connect-timeout: 5000
|
connect-timeout: 5000
|
||||||
read-timeout: 120000
|
read-timeout: 120000
|
||||||
|
http:
|
||||||
|
root-path: /api/v1
|
||||||
|
|||||||
623
src/main/resources/openapi-roleplay-public-v1.yml
Normal file
623
src/main/resources/openapi-roleplay-public-v1.yml
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: RolePlay API
|
||||||
|
description: |
|
||||||
|
Public REST API for the RolePlay service. Provides endpoints to discover
|
||||||
|
available AI models, manage role-play sessions, and submit user turns.
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: RolePlay Project
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /api/v1
|
||||||
|
description: Default server base path
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: models
|
||||||
|
description: Discover available Ollama models
|
||||||
|
- name: sessions
|
||||||
|
description: Manage role-play sessions
|
||||||
|
- name: turns
|
||||||
|
description: Submit user actions within a session
|
||||||
|
|
||||||
|
paths:
|
||||||
|
|
||||||
|
/models:
|
||||||
|
get:
|
||||||
|
operationId: listModels
|
||||||
|
summary: List available AI models
|
||||||
|
description: Returns all Ollama models currently installed on the backend server.
|
||||||
|
tags:
|
||||||
|
- models
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response with a list of available models.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ModelListResponse'
|
||||||
|
"500":
|
||||||
|
description: Server error while contacting Ollama.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/sessions:
|
||||||
|
post:
|
||||||
|
operationId: createSession
|
||||||
|
summary: Start a new role-play session
|
||||||
|
description: |
|
||||||
|
Initialises a new session with the given model and optional scenario
|
||||||
|
setup. The backend performs the two-call Ollama pattern (narrative +
|
||||||
|
state extraction) and returns the opening scene.
|
||||||
|
tags:
|
||||||
|
- sessions
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateSessionRequest'
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Session created; opening narrative and initial state returned.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SessionResponse'
|
||||||
|
"400":
|
||||||
|
description: Invalid request body.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Server error during session initialisation.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/sessions/{sessionId}:
|
||||||
|
get:
|
||||||
|
operationId: getSession
|
||||||
|
summary: Retrieve a session
|
||||||
|
description: Returns the full current state of an existing role-play session.
|
||||||
|
tags:
|
||||||
|
- sessions
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/SessionId'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Session found and returned.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SessionResponse'
|
||||||
|
"404":
|
||||||
|
description: Session not found.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
/sessions/{sessionId}/turns:
|
||||||
|
post:
|
||||||
|
operationId: submitTurn
|
||||||
|
summary: Submit a user action for the next turn
|
||||||
|
description: |
|
||||||
|
Submits the user's action (speech, physical action, or a chosen
|
||||||
|
suggestion) and an optional narrative recommendation. The backend
|
||||||
|
executes the two-call Ollama pattern and returns the resulting
|
||||||
|
narrative and updated suggestions.
|
||||||
|
tags:
|
||||||
|
- turns
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/SessionId'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TurnRequest'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Turn processed; narrative and new suggestions returned.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TurnResponse'
|
||||||
|
"400":
|
||||||
|
description: Invalid request body.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Session not found.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Server error during turn processing.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
components:
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
SessionId:
|
||||||
|
name: sessionId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Unique identifier of the role-play session.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
|
||||||
|
# ─── Model discovery ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ModelInfo:
|
||||||
|
type: object
|
||||||
|
description: Metadata about a single Ollama model.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Technical model identifier (e.g. "llama3:latest").
|
||||||
|
example: llama3:latest
|
||||||
|
displayName:
|
||||||
|
type: string
|
||||||
|
description: Human-readable model name derived from the identifier.
|
||||||
|
example: Llama 3 (latest)
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
description: Model size in bytes.
|
||||||
|
example: 4000000000
|
||||||
|
family:
|
||||||
|
type: string
|
||||||
|
description: Model family (e.g. "llama").
|
||||||
|
example: llama
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
|
||||||
|
ModelListResponse:
|
||||||
|
type: object
|
||||||
|
description: Response containing all available models.
|
||||||
|
properties:
|
||||||
|
models:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ModelInfo'
|
||||||
|
required:
|
||||||
|
- models
|
||||||
|
|
||||||
|
# ─── Session ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
CreateSessionRequest:
|
||||||
|
type: object
|
||||||
|
description: Request body for creating a new role-play session.
|
||||||
|
properties:
|
||||||
|
model:
|
||||||
|
type: string
|
||||||
|
description: The Ollama model name to use for this session.
|
||||||
|
example: llama3:latest
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
description: BCP-47 language tag for the session (default "en").
|
||||||
|
example: en
|
||||||
|
default: en
|
||||||
|
safetyLevel:
|
||||||
|
type: string
|
||||||
|
description: Content safety level.
|
||||||
|
enum:
|
||||||
|
- standard
|
||||||
|
- strict
|
||||||
|
default: standard
|
||||||
|
scenario:
|
||||||
|
$ref: '#/components/schemas/ScenarioSetup'
|
||||||
|
required:
|
||||||
|
- model
|
||||||
|
|
||||||
|
ScenarioSetup:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Optional initial scenario definition. If omitted the backend uses
|
||||||
|
built-in defaults.
|
||||||
|
properties:
|
||||||
|
setting:
|
||||||
|
type: string
|
||||||
|
description: Place, time, and atmosphere description.
|
||||||
|
example: "A fog-covered harbour at dawn, 1923"
|
||||||
|
initialConflict:
|
||||||
|
type: string
|
||||||
|
description: The hook or starting conflict.
|
||||||
|
example: "Strange noises from the cargo hold"
|
||||||
|
userCharacter:
|
||||||
|
$ref: '#/components/schemas/CharacterDefinition'
|
||||||
|
aiCharacters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CharacterDefinition'
|
||||||
|
|
||||||
|
CharacterDefinition:
|
||||||
|
type: object
|
||||||
|
description: Definition of a character for session initialisation.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique character identifier.
|
||||||
|
example: captain_morgan
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Display name.
|
||||||
|
example: Captain Morgan
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: Narrative role.
|
||||||
|
example: ship captain
|
||||||
|
backstory:
|
||||||
|
type: string
|
||||||
|
description: Character backstory (AI characters only).
|
||||||
|
personalityTraits:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Personality descriptors (AI characters only).
|
||||||
|
speakingStyle:
|
||||||
|
type: string
|
||||||
|
description: Tone and speech quirks (AI characters only).
|
||||||
|
goals:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Short- and long-term goals (AI characters only).
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- role
|
||||||
|
|
||||||
|
SessionResponse:
|
||||||
|
type: object
|
||||||
|
description: Current state of a role-play session.
|
||||||
|
properties:
|
||||||
|
sessionId:
|
||||||
|
type: string
|
||||||
|
description: Unique session identifier.
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
model:
|
||||||
|
type: string
|
||||||
|
description: Ollama model in use.
|
||||||
|
example: llama3:latest
|
||||||
|
language:
|
||||||
|
type: string
|
||||||
|
description: Session language.
|
||||||
|
example: en
|
||||||
|
safetyLevel:
|
||||||
|
type: string
|
||||||
|
description: Content safety level.
|
||||||
|
example: standard
|
||||||
|
narrative:
|
||||||
|
type: string
|
||||||
|
description: Most recent narrative text (opening scene or latest turn).
|
||||||
|
situation:
|
||||||
|
$ref: '#/components/schemas/SituationState'
|
||||||
|
characters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CharacterState'
|
||||||
|
suggestions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Suggestion'
|
||||||
|
turnNumber:
|
||||||
|
type: integer
|
||||||
|
description: Current turn counter (0 = just initialised).
|
||||||
|
example: 0
|
||||||
|
required:
|
||||||
|
- sessionId
|
||||||
|
- model
|
||||||
|
- language
|
||||||
|
- safetyLevel
|
||||||
|
- turnNumber
|
||||||
|
|
||||||
|
SituationState:
|
||||||
|
type: object
|
||||||
|
description: Current world and scene state within a session.
|
||||||
|
properties:
|
||||||
|
setting:
|
||||||
|
type: string
|
||||||
|
description: Place, time, and atmosphere.
|
||||||
|
currentScene:
|
||||||
|
type: string
|
||||||
|
description: What is currently in focus for the scene.
|
||||||
|
timeline:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Ordered list of important events so far.
|
||||||
|
openThreads:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Unresolved plot lines, mysteries, or quests.
|
||||||
|
externalPressures:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Time-sensitive pressures such as deadlines or approaching threats.
|
||||||
|
worldStateFlags:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
description: Boolean or tag-style flags representing key world states.
|
||||||
|
|
||||||
|
CharacterState:
|
||||||
|
type: object
|
||||||
|
description: Current dynamic state of a single character within a session.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique character identifier.
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Display name.
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
description: Narrative role.
|
||||||
|
isUserCharacter:
|
||||||
|
type: boolean
|
||||||
|
description: Whether this is the user-controlled character.
|
||||||
|
currentMood:
|
||||||
|
type: string
|
||||||
|
description: Current emotional state.
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: Physical or narrative status.
|
||||||
|
knowledge:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Facts the character has learned.
|
||||||
|
relationships:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Map from other character IDs to relationship descriptions.
|
||||||
|
recentActionsSummary:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Brief summary of recent behaviour.
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- isUserCharacter
|
||||||
|
|
||||||
|
# ─── Turn ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TurnRequest:
|
||||||
|
type: object
|
||||||
|
description: User action submitted for the next story turn.
|
||||||
|
properties:
|
||||||
|
userAction:
|
||||||
|
$ref: '#/components/schemas/UserActionRequest'
|
||||||
|
recommendation:
|
||||||
|
$ref: '#/components/schemas/RecommendationRequest'
|
||||||
|
required:
|
||||||
|
- userAction
|
||||||
|
|
||||||
|
UserActionRequest:
|
||||||
|
type: object
|
||||||
|
description: The action the user performed this turn.
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Type of user action.
|
||||||
|
enum:
|
||||||
|
- speech
|
||||||
|
- action
|
||||||
|
- choice
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
description: Free-text content of the action or dialogue.
|
||||||
|
selectedSuggestionId:
|
||||||
|
type: string
|
||||||
|
description: If the user chose a suggestion, its ID; otherwise omitted.
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
|
||||||
|
RecommendationRequest:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
Optional narrative guidance the user attaches to a turn. Treated as a
|
||||||
|
soft hint by the model.
|
||||||
|
properties:
|
||||||
|
desiredTone:
|
||||||
|
type: string
|
||||||
|
description: Desired mood for the next beat (e.g. "tense", "humorous").
|
||||||
|
example: tense
|
||||||
|
preferredDirection:
|
||||||
|
type: string
|
||||||
|
description: Free-text direction hint.
|
||||||
|
example: "The captain should reveal a partial truth"
|
||||||
|
focusCharacters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Character IDs that should play a prominent role.
|
||||||
|
|
||||||
|
TurnResponse:
|
||||||
|
type: object
|
||||||
|
description: Result of processing a user turn.
|
||||||
|
properties:
|
||||||
|
turnNumber:
|
||||||
|
type: integer
|
||||||
|
description: The turn number that was just completed.
|
||||||
|
example: 1
|
||||||
|
narrative:
|
||||||
|
type: string
|
||||||
|
description: Free-form narrative text produced by the model for this turn.
|
||||||
|
characterResponses:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CharacterResponseItem'
|
||||||
|
description: Per-character breakdown of what each AI character said or did.
|
||||||
|
updatedSituation:
|
||||||
|
$ref: '#/components/schemas/SituationUpdate'
|
||||||
|
updatedCharacters:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CharacterUpdate'
|
||||||
|
suggestions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Suggestion'
|
||||||
|
required:
|
||||||
|
- turnNumber
|
||||||
|
- narrative
|
||||||
|
|
||||||
|
CharacterResponseItem:
|
||||||
|
type: object
|
||||||
|
description: What a single AI character said or did during a turn.
|
||||||
|
properties:
|
||||||
|
characterId:
|
||||||
|
type: string
|
||||||
|
description: ID of the character who acted.
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Type of response.
|
||||||
|
enum:
|
||||||
|
- speech
|
||||||
|
- action
|
||||||
|
- reaction
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
description: Quoted dialogue if type is "speech", otherwise null.
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
description: Description of physical action, or null if none.
|
||||||
|
moodAfter:
|
||||||
|
type: string
|
||||||
|
description: The character's mood after this turn.
|
||||||
|
required:
|
||||||
|
- characterId
|
||||||
|
- type
|
||||||
|
|
||||||
|
SituationUpdate:
|
||||||
|
type: object
|
||||||
|
description: Diff-style situation changes for a single turn.
|
||||||
|
properties:
|
||||||
|
currentScene:
|
||||||
|
type: string
|
||||||
|
description: Updated scene description.
|
||||||
|
newTimelineEntries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: New events to append to the timeline.
|
||||||
|
openThreadsChanges:
|
||||||
|
$ref: '#/components/schemas/OpenThreadsChanges'
|
||||||
|
worldStateFlags:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
description: Current world state flags after this turn.
|
||||||
|
|
||||||
|
OpenThreadsChanges:
|
||||||
|
type: object
|
||||||
|
description: Diff-style changes to open narrative threads.
|
||||||
|
properties:
|
||||||
|
added:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: New threads introduced this turn.
|
||||||
|
resolved:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Threads that were resolved or closed this turn.
|
||||||
|
|
||||||
|
CharacterUpdate:
|
||||||
|
type: object
|
||||||
|
description: Diff-style character state changes for a single character.
|
||||||
|
properties:
|
||||||
|
characterId:
|
||||||
|
type: string
|
||||||
|
description: ID of the character whose state changed.
|
||||||
|
currentMood:
|
||||||
|
type: string
|
||||||
|
description: The character's mood after this turn.
|
||||||
|
knowledgeGained:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: New facts the character learned this turn.
|
||||||
|
relationshipChanges:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Changed relationships (character ID to new descriptor).
|
||||||
|
required:
|
||||||
|
- characterId
|
||||||
|
|
||||||
|
Suggestion:
|
||||||
|
type: object
|
||||||
|
description: A suggestion for what could happen next in the story.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique suggestion identifier.
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: Category of the suggestion.
|
||||||
|
enum:
|
||||||
|
- player_action
|
||||||
|
- world_event
|
||||||
|
- npc_action
|
||||||
|
- twist
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: Short human-readable label.
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
description: One- or two-sentence explanation.
|
||||||
|
consequences:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: Brief list of possible outcomes.
|
||||||
|
riskLevel:
|
||||||
|
type: string
|
||||||
|
description: How risky this option is for the player.
|
||||||
|
enum:
|
||||||
|
- low
|
||||||
|
- medium
|
||||||
|
- high
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- type
|
||||||
|
- title
|
||||||
|
|
||||||
|
# ─── Error ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
description: Standard error response body.
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: Machine-readable error code.
|
||||||
|
example: SESSION_NOT_FOUND
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: Human-readable error description.
|
||||||
|
example: "No session found with the given ID"
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
- message
|
||||||
|
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
package de.neitzel.roleplay.business;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.model.CreateSessionRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.SessionResponse;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnRequest;
|
||||||
|
import de.neitzel.roleplay.fascade.model.TurnResponse;
|
||||||
|
import de.neitzel.roleplay.fascade.model.UserActionRequest;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link InMemorySessionService}.
|
||||||
|
*/
|
||||||
|
class InMemorySessionServiceTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance under test – no CDI dependencies to mock.
|
||||||
|
*/
|
||||||
|
private InMemorySessionService sessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a fresh service instance before each test to ensure isolation.
|
||||||
|
*/
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
sessionService = new InMemorySessionService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that creating a session returns a response with a non-null ID,
|
||||||
|
* the correct model name, and turn number 0.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void createSessionReturnsSessionWithCorrectMetadata() {
|
||||||
|
// Arrange
|
||||||
|
CreateSessionRequest request = new CreateSessionRequest("llama3:latest");
|
||||||
|
request.setLanguage("de");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
SessionResponse response = sessionService.createSession(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response.getSessionId());
|
||||||
|
assertFalse(response.getSessionId().isBlank());
|
||||||
|
assertEquals("llama3:latest", response.getModel());
|
||||||
|
assertEquals("de", response.getLanguage());
|
||||||
|
assertEquals(0, response.getTurnNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that a created session can be retrieved by its ID.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void getSessionReturnsPreviouslyCreatedSession() {
|
||||||
|
// Arrange
|
||||||
|
CreateSessionRequest request = new CreateSessionRequest("llama3:latest");
|
||||||
|
SessionResponse created = sessionService.createSession(request);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Optional<SessionResponse> found = sessionService.getSession(created.getSessionId());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(found.isPresent());
|
||||||
|
assertEquals(created.getSessionId(), found.get().getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that retrieving an unknown session returns an empty Optional.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void getSessionReturnsEmptyForUnknownId() {
|
||||||
|
// Act
|
||||||
|
Optional<SessionResponse> result = sessionService.getSession("unknown-session-id");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that submitting a turn increments the turn number and returns
|
||||||
|
* a turn response for the correct turn.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void submitTurnIncreasesTurnNumber() {
|
||||||
|
// Arrange
|
||||||
|
CreateSessionRequest createRequest = new CreateSessionRequest("llama3:latest");
|
||||||
|
SessionResponse session = sessionService.createSession(createRequest);
|
||||||
|
|
||||||
|
UserActionRequest action = new UserActionRequest();
|
||||||
|
action.setType(UserActionRequest.TypeEnum.ACTION);
|
||||||
|
action.setContent("I look around the room.");
|
||||||
|
TurnRequest turnRequest = new TurnRequest(action);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Optional<TurnResponse> response = sessionService.submitTurn(session.getSessionId(), turnRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(response.isPresent());
|
||||||
|
assertEquals(1, response.get().getTurnNumber());
|
||||||
|
assertNotNull(response.get().getNarrative());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that submitting a turn for an unknown session returns an empty Optional.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void submitTurnReturnsEmptyForUnknownSession() {
|
||||||
|
// Arrange
|
||||||
|
UserActionRequest action = new UserActionRequest();
|
||||||
|
action.setType(UserActionRequest.TypeEnum.SPEECH);
|
||||||
|
action.setContent("Hello!");
|
||||||
|
TurnRequest turnRequest = new TurnRequest(action);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Optional<TurnResponse> response = sessionService.submitTurn("non-existent", turnRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(response.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the turn number increments correctly across multiple turns.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void multipleSuccessiveTurnsIncrementTurnNumber() {
|
||||||
|
// Arrange
|
||||||
|
CreateSessionRequest createRequest = new CreateSessionRequest("llama3:latest");
|
||||||
|
SessionResponse session = sessionService.createSession(createRequest);
|
||||||
|
String sessionId = session.getSessionId();
|
||||||
|
|
||||||
|
UserActionRequest action = new UserActionRequest();
|
||||||
|
action.setType(UserActionRequest.TypeEnum.ACTION);
|
||||||
|
action.setContent("Next action.");
|
||||||
|
TurnRequest turnRequest = new TurnRequest(action);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
sessionService.submitTurn(sessionId, turnRequest);
|
||||||
|
Optional<TurnResponse> secondTurn = sessionService.submitTurn(sessionId, turnRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertTrue(secondTurn.isPresent());
|
||||||
|
assertEquals(2, secondTurn.get().getTurnNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the session language defaults to "en" when not specified.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void createSessionUsesDefaultLanguageWhenNotProvided() {
|
||||||
|
// Arrange
|
||||||
|
CreateSessionRequest request = new CreateSessionRequest("llama3:latest");
|
||||||
|
// language intentionally left null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
SessionResponse response = sessionService.createSession(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals("en", response.getLanguage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
src/test/java/de/neitzel/roleplay/business/ModelServiceTest.java
Normal file
130
src/test/java/de/neitzel/roleplay/business/ModelServiceTest.java
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package de.neitzel.roleplay.business;
|
||||||
|
|
||||||
|
import de.neitzel.roleplay.fascade.OllamaClient;
|
||||||
|
import de.neitzel.roleplay.fascade.OllamaModelDetails;
|
||||||
|
import de.neitzel.roleplay.fascade.OllamaModelInfo;
|
||||||
|
import de.neitzel.roleplay.fascade.model.ModelInfo;
|
||||||
|
import de.neitzel.roleplay.fascade.model.ModelListResponse;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link ModelService}.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ModelServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OllamaClient ollamaClient;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ModelService modelService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that models returned by the Ollama client are mapped correctly
|
||||||
|
* to the API response format.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void listModelsMapsFamilyAndDisplayName() {
|
||||||
|
// Arrange
|
||||||
|
OllamaModelDetails details = OllamaModelDetails.builder()
|
||||||
|
.family("llama")
|
||||||
|
.parameterSize("8B")
|
||||||
|
.build();
|
||||||
|
OllamaModelInfo ollamaModel = OllamaModelInfo.builder()
|
||||||
|
.name("llama3:latest")
|
||||||
|
.model("llama3:latest")
|
||||||
|
.size(4_000_000_000L)
|
||||||
|
.details(details)
|
||||||
|
.build();
|
||||||
|
when(ollamaClient.listModels()).thenReturn(List.of(ollamaModel));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelListResponse response = modelService.listModels();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals(1, response.getModels().size());
|
||||||
|
ModelInfo info = response.getModels().get(0);
|
||||||
|
assertEquals("llama3:latest", info.getName());
|
||||||
|
assertEquals("llama", info.getFamily());
|
||||||
|
assertEquals(4_000_000_000L, info.getSize());
|
||||||
|
assertEquals("Llama3 (latest)", info.getDisplayName());
|
||||||
|
verify(ollamaClient).listModels();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that a model name without a tag receives a display name equal
|
||||||
|
* to the capitalised base name.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void listModelsDerivesDisplayNameWithoutTag() {
|
||||||
|
// Arrange
|
||||||
|
OllamaModelInfo ollamaModel = OllamaModelInfo.builder()
|
||||||
|
.name("mistral")
|
||||||
|
.model("mistral")
|
||||||
|
.size(2_000_000_000L)
|
||||||
|
.build();
|
||||||
|
when(ollamaClient.listModels()).thenReturn(List.of(ollamaModel));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelListResponse response = modelService.listModels();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals("Mistral", response.getModels().get(0).getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the model list is empty when the Ollama client returns no
|
||||||
|
* models.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void listModelsReturnsEmptyListWhenNoModelsAvailable() {
|
||||||
|
// Arrange
|
||||||
|
when(ollamaClient.listModels()).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelListResponse response = modelService.listModels();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertNotNull(response);
|
||||||
|
assertTrue(response.getModels().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that a model without details does not cause a NullPointerException
|
||||||
|
* and maps the family as null.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void listModelsHandlesModelWithoutDetails() {
|
||||||
|
// Arrange
|
||||||
|
OllamaModelInfo ollamaModel = OllamaModelInfo.builder()
|
||||||
|
.name("phi3:mini")
|
||||||
|
.model("phi3:mini")
|
||||||
|
.size(1_000_000_000L)
|
||||||
|
.build();
|
||||||
|
when(ollamaClient.listModels()).thenReturn(List.of(ollamaModel));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
ModelListResponse response = modelService.listModels();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
ModelInfo info = response.getModels().get(0);
|
||||||
|
assertNull(info.getFamily());
|
||||||
|
assertEquals("Phi3 (mini)", info.getDisplayName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user