diff --git a/docs/src/modules/ROOT/images/commercial-editions/license-manager-view-licenses.png b/docs/src/modules/ROOT/images/commercial-editions/license-manager-view-licenses.png new file mode 100644 index 00000000000..9513e5ecd00 Binary files /dev/null and b/docs/src/modules/ROOT/images/commercial-editions/license-manager-view-licenses.png differ diff --git a/docs/src/modules/ROOT/nav.adoc b/docs/src/modules/ROOT/nav.adoc index 4357f247009..6306d18ee0a 100644 --- a/docs/src/modules/ROOT/nav.adoc +++ b/docs/src/modules/ROOT/nav.adoc @@ -38,6 +38,12 @@ ** xref:running-timefold-solver/benchmarking-and-tweaking.adoc[leveloffset=+1] ** xref:running-timefold-solver/solver-diagnostics.adoc[leveloffset=+1] +* Deploying to the Timefold Platform +** xref:deploying-to-platform/introduction.adoc[Overview] +** xref:deploying-to-platform/getting-started.adoc[Getting started] +** xref:deploying-to-platform/model-metadata.adoc[leveloffset=+1] +** xref:deploying-to-platform/metrics.adoc[leveloffset=+1] + * Optimization algorithms ** xref:optimization-algorithms/overview.adoc[Overview] ** xref:optimization-algorithms/construction-heuristics.adoc[leveloffset=+1] @@ -81,3 +87,4 @@ ** xref:constraints-and-score/performance.adoc#constraintProfiling[Constraint profiling] ** xref:commercial-editions/multistage-moves.adoc[leveloffset=+1] ** xref:running-timefold-solver/library/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] +** xref:commercial-editions/license-management.adoc[License management] diff --git a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc index ed7d2f9551c..44b3f27b70f 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/commercial-editions.adoc @@ -14,10 +14,11 @@ TIP: Looking for quicker time-to-value? Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. Just plug into our API and start optimizing immediately. +[#_free_trials_and_licenses] == Free trials and licenses -We offer free trials to everyone as well as free licenses to non-profit organizations and for academic research. -See our https://licenses.timefold.ai/[license portal] to get your license, then follow the xref:commercial-editions/installation.adoc[installation guide] to get started. +Every new account on the https://licenses.timefold.ai/[Timefold License Manager] starts with a free, time-limited Enterprise trial, so you can create and download fully functional Enterprise Edition license files without talking to sales first. +We also offer free licenses to non-profit organizations and for academic research. == Features @@ -52,4 +53,4 @@ See our https://licenses.timefold.ai/[license portal] to get your license, then | xref:running-timefold-solver/library/library-integration.adoc#throttlingBestSolutionEvents[Throttling best solution events] | ✓ -|=== \ No newline at end of file +|=== diff --git a/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc b/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc index 4b255fdc382..c52fe4c70f6 100644 --- a/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc +++ b/docs/src/modules/ROOT/pages/commercial-editions/installation.adoc @@ -13,7 +13,8 @@ A correctly configured and active license key is required in order for our comme [#solverObtainLicenseKey] === Obtaining a license key -Generate your license key using https://licenses.timefold.ai/[Timefold License Manager]. +Generate your license key using the xref:commercial-editions/license-management.adoc[Timefold License Manager]. +Every new account starts with a free, time-limited Enterprise trial, so you don't need to talk to sales to get started. Take care not to leak the license file, for example by committing it to a public repository, logging the license file content in logs, or sharing it with unauthorized parties. diff --git a/docs/src/modules/ROOT/pages/commercial-editions/license-management.adoc b/docs/src/modules/ROOT/pages/commercial-editions/license-management.adoc new file mode 100644 index 00000000000..960fc83fe49 --- /dev/null +++ b/docs/src/modules/ROOT/pages/commercial-editions/license-management.adoc @@ -0,0 +1,108 @@ += License management +:doctype: book +:icons: font + +The https://licenses.timefold.ai/[Timefold License Manager] is where you register for a free trial, add and download license files, and manage your team's access to Timefold Solver Enterprise Edition. + +== Register and free trial + +. Go to https://licenses.timefold.ai/[licenses.timefold.ai] and click *Register*. +. Log in with your company email address. +Public email domains (Gmail, Hotmail, Yahoo, and similar) aren't accepted. +You must use a work email. +. Fill in the registration form: company name and website URL, the scheduling problem you're solving, the scale of your problem, and how you currently solve it. +. Accept the terms of service and click *Register*. + +Your account and company are created immediately, and you're given a free, time-limited Enterprise trial: you can create and download fully functional Enterprise Edition license files right away, without talking to sales first. + +The first user to register for a company automatically becomes that company's *admin*. + +When the trial ends, you need a paid subscription to keep creating new licenses. + +== Managing licenses + +Creating licenses requires the *admin* role within your company. +Regular users can view and download their own licenses only. + +=== View your licenses + +Click *Licenses* in the sidebar to see your active licenses, or switch to the *Expired* tab to see past ones. +Each license card shows its type, expiration date, and a download button. + +.The Licenses list in the Timefold License Manager +image::commercial-editions/license-manager-view-licenses.png[The Licenses list in the Timefold License Manager, showing active license cards with type, expiration date, and a download button] + +=== Add a license + +. Click *Licenses* → *Add license key*. +. Select an expiration date. +It can't exceed your company's contract end date. +. Optionally, assign the license to a specific user in your company. +. Click *Add license key*. + +Your company's plan determines the license type; you don't need to select it manually. + +=== Download a license key + +Click *Download license key* next to a license to get its `.pem` file. +Introduce it to your project as described in xref:commercial-editions/installation.adoc#solverSetupLicenseKey[Set up your license key]. + +[IMPORTANT] +==== +Treat your license key as a secret. +Don't commit it to source control; inject it at runtime using your secret manager instead (for example Kubernetes Secrets, AWS Secrets Manager, HashiCorp Vault, or a CI/CD secret store). +==== + +=== Expired licenses + +Expired licenses remain visible under the *Expired* tab, and you can still download them to review, but they're no longer accepted by the solver. + +[IMPORTANT] +==== +Once a license expires, Timefold Solver refuses to start. +There is no grace period. +Renew your license before it expires to avoid downtime. +==== + +== Notifications + +=== Email reminders + +The license manager emails the license owner before a license expires: seven days before, and again one day before. +If you're not receiving these emails, check your spam folder or ask your company admin to verify the license owner's address. + +=== Solver warnings + +Timefold Solver also logs warnings as a license approaches expiry, with increasing urgency at less than 30 days, less than 14 days, and less than one day remaining. +Once expired, the solver refuses to start. +Monitor your application logs for these warnings so you have time to renew. + +== Managing your team + +Inviting and managing team members requires the *admin* role within your company. + +=== Invite team members + +Open your company from the sidebar, go to the *Users* tab, and click *Invite user*. +The invitee receives an email with an acceptance link valid for 14 days. +Once they accept, they join your company with the *user* role. + +If an invitation expires or wasn't received, find it in the *Users* tab and click *Resend* to send a fresh 14-day link. + +=== Change a user's role or remove a user + +From the *Users* tab, click *Edit* next to a user to change their role between *admin* and *user*, or click *Delete* to remove them. + +NOTE: At least one admin must remain in a company at all times; you can't demote or delete the last admin. + +== Account and company settings + +Update your name from your avatar menu → *Settings*. +Your identity provider manages your email address, so you can't change it there. + +Only Timefold can change company-level settings, such as your plan and contract dates. +Contact mailto:support@timefold.ai[support@timefold.ai] if you need your plan updated. + +== Pricing + +Visit https://licenses.timefold.ai/pricing[the pricing page] for current Enterprise pricing and contact details. diff --git a/docs/src/modules/ROOT/pages/deploying-to-platform/_preview-note.adoc b/docs/src/modules/ROOT/pages/deploying-to-platform/_preview-note.adoc new file mode 100644 index 00000000000..c014451d1ce --- /dev/null +++ b/docs/src/modules/ROOT/pages/deploying-to-platform/_preview-note.adoc @@ -0,0 +1,7 @@ +[IMPORTANT] +==== +Deploying custom models to the platform is in preview. + +This is currently only available to a limited set of partners. +If you're interested in joining this preview program, mailto:info@timefold.ai[get in touch with the Timefold team] to discuss access. +==== diff --git a/docs/src/modules/ROOT/pages/deploying-to-platform/getting-started.adoc b/docs/src/modules/ROOT/pages/deploying-to-platform/getting-started.adoc new file mode 100644 index 00000000000..111e317ffce --- /dev/null +++ b/docs/src/modules/ROOT/pages/deploying-to-platform/getting-started.adoc @@ -0,0 +1,136 @@ += Getting started: deploying to the platform (Preview) +:description: Deploy the service you built to Timefold Platform +:doctype: book +:sectnums: +:icons: font + +include::../_attributes.adoc[] + +This guide continues from xref:quickstart/service/getting-started.adoc[Getting started: building a service], where you built the School Timetabling model as a service. +It walks you through deploying that same model to Timefold Platform, so it can be reached through a managed, multi-tenant REST API instead of `mvn quarkus:dev` on your own machine. + +For the reasoning behind why you'd want to do this, see xref:deploying-to-platform/introduction.adoc[Deploying to the Timefold Platform]. + +include::_preview-note.adoc[] + +[sectnums!] +== Prerequisites + +To complete this guide, you need: + +* The School Timetabling service built in xref:quickstart/service/getting-started.adoc[Getting started: building a service]. +* A Timefold Platform account with tenant-admin access to the tenant you want to register the model in. +* A Personal Access Token (PAT), scoped to the tenant you want to register the model in, with *Create*, *Read*, *Update*, and *Delete* permissions on the *Registered Model* resource. +See xref:#_generate_a_personal_access_token[Generate a Personal Access Token] below. +* Push access to Timefold's container registry, since the `configure` goal builds and pushes your model's container image there. +This access needs to be requested; mailto:info@timefold.ai[get in touch with the Timefold team] before you try to deploy. + +NOTE: Timefold Platform requires your model to be built with the Enterprise Edition of Timefold Solver, but building doesn't require a license, only running it does, and the platform provides that license when your model runs there. See xref:#_enterprise_edition[Build with the Enterprise Edition] below. + +[#_add_the_maven_plugin] +== Add the Maven plugin + +The `timefold-maven-plugin` connects your build to the platform's container registry and handles registering your model. +Add it to your `pom.xml`, and configure it with your `platformUrl`, a registration `key` for your model, and the tenant(s) to register it under. + +Your `platformUrl` will typically be `https://app.timefold.ai`. +(See https://docs.timefold.ai/timefold-platform/latest/api/integration-scenarios/integration-scenarios#_our_recommendation[Integration scenarios] if you're deploying to a different installation, for example a self-hosted platform or a different data residency region.) + +Also set `handleSubscription` to `true`, so your tenant gets subscribed to the model when you deploy it; see xref:#_what_happens_on_deploy[What happens on deploy] below. + +Follow the setup instructions in the https://github.com/TimefoldAI/timefold-solver/blob/main/service/tools/maven-plugin/README.adoc[Maven plugin documentation] to add and configure it. + +[#_enterprise_edition] +== Build with the Enterprise Edition + +Timefold Platform requires your model to be built with the Enterprise Edition of Timefold Solver; a Community Edition build cannot be deployed. +By default, your build uses the Community Edition, so you need to opt in explicitly. +This also gets you the xref:deploying-to-platform/introduction.adoc#_enterprise_edition_features[Enterprise Edition features] surfaced by the platform, such as score analysis. + +Build with `-Denterprise=true` to activate the `enterprise` Maven profile defined in `timefold-solver-service-parent`, which pulls in the Enterprise Edition dependencies: + +[source,bash,options="nowrap"] +---- +mvn clean package -Denterprise=true +---- + +The Enterprise Edition requires a license key to function at runtime, as described in xref:commercial-editions/installation.adoc[Installation]. +Building with the Enterprise Edition does not require a license; a license is only checked when the model actually runs. +When you deploy to Timefold Platform, the platform provides that license for you, so you don't need one of your own just to follow this guide. + +You still may want a license if you want to run your Enterprise Edition build locally, for example to test the model with `mvn quarkus:dev` before deploying it. +Every new account on the xref:commercial-editions/license-management.adoc[Timefold License Manager] starts with a free, time-limited Enterprise trial, so you can create and download fully functional Enterprise Edition license files without talking to sales first. +We also offer free licenses to non-profit organizations and for academic research. + +[#_authenticate] +== Authenticate + +[#_generate_a_personal_access_token] +=== Generate a Personal Access Token + +Generate a PAT from your user menu on the platform (*User menu → Access tokens → Generate new token*). +When selecting its permissions, scope the token to the tenant you want to register the model in, and grant *Create*, *Read*, *Update*, and *Delete* on the *Registered Model* resource, since the plugin needs all four to register, update, and later undeploy your model. + +See https://docs.timefold.ai/timefold-platform/latest/api/platform-api#_authentication_with_personal_access_tokens[Authentication with Personal Access Tokens] for the full walkthrough, including screenshots of the token generation dialog. + +=== Export the token + +The plugin reads your PAT from the `TIMEFOLD_PAT` environment variable. +Export it before building: + +[source,bash,options="nowrap"] +---- +export TIMEFOLD_PAT= +---- + +[#_build_and_deploy] +== Build and deploy + +Build and deploy your model in a single command. +`-Denterprise=true` is required, since the platform only accepts models built with the Enterprise Edition; see xref:#_enterprise_edition[Build with the Enterprise Edition] above. + +[source,bash,options="nowrap"] +---- +mvn clean package -Denterprise=true timefold:deploy +---- + +=== Preview without pushing container +To preview what would happen without actually pushing the container image or registering the model, add `-Dtimefold.dryRun=true`. +This logs the requests that would have been made instead of sending them. + +[#_what_happens_on_deploy] +=== What happens on deploy + +During `package`, the build generates a model descriptor (`target/timefold/timefold-model-descriptor.json`) and packages it together with the OpenAPI specification, JSON schemas, and default configuration profile into `target/model-descriptor.zip`. + +The `timefold:deploy` goal uploads that archive and registers it against the platform's model registration endpoint (`POST /api/platform/v1/models`), using the `key` and `tenants` from the plugin configuration. + +If a model with the same `key` is already registered, the deploy fails with a conflict (HTTP 409) unless `overwrite` is enabled, in which case the existing registration is updated instead (`PATCH /api/platform/v1/models/`). +You can also set this from the command line with `-Dtimefold.model.overwrite=true`. + +Registering a model does not automatically subscribe your tenant to it; with `handleSubscription` set to `true`, the plugin also subscribes your tenant when it registers the model. +Without it, your tenant won't see the model on the platform after you deploy it. + +[#_verify_on_the_platform] +== Verify on the platform + +Log in to the platform UI at your `platformUrl` and find the model under your tenant. +Before considering the model ready for review, confirm the following: + +* *The model shows up in your tenant*: go to *Manage tenant → Models* and confirm your model is listed, with the version details (major API version, and the current minor version's build time, commit, and solver version) matching what you just deployed. +If it's missing, double check that `handleSubscription` is set to `true` in the Maven plugin configuration. +* *It solves*: submit your xref:running-timefold-solver/service/demo-data.adoc[demo dataset] and confirm the solve completes with a reasonable score. +* *The output looks right*: inspect the resulting schedule and check that lessons are assigned sensible timeslots and rooms. +* *Constraints show up by name in score analysis*: the score analysis breakdown should show your constraints under human-readable names, not raw identifiers. +This requires the richer constraint metadata described in xref:deploying-to-platform/model-metadata.adoc#_constraint_descriptions_and_groups[Constraint descriptions and groups]. +* *Input and output metrics show up*: the dataset view should display the metrics you exposed, as described in xref:deploying-to-platform/metrics.adoc[Using metrics]. +* *You can add a configuration profile*: add a new configuration profile in the UI and override one of your xref:running-timefold-solver/service/constraint-overrides.adoc[constraint weights], then confirm a re-solve picks up the new weight. +See xref:deploying-to-platform/model-metadata.adoc#_configuration_profiles[Configuration profiles] for the properties that define the default profile shipped with your model. + +[sectnums!] +== Next + +* Learn about the platform-specific metadata that shapes your model's UI experience: xref:deploying-to-platform/model-metadata.adoc[Platform model metadata]. +* Make solve graphs, comparison, and Insights more useful by adding xref:deploying-to-platform/metrics.adoc[input and output metrics]. +* Read more about xref:running-timefold-solver/service/overview.adoc[Building a service]. +* See the https://github.com/TimefoldAI/timefold-solver/blob/main/service/tools/maven-plugin/README.adoc[Maven plugin documentation] for the full set of goals and parameters, including how to undeploy a model. diff --git a/docs/src/modules/ROOT/pages/deploying-to-platform/introduction.adoc b/docs/src/modules/ROOT/pages/deploying-to-platform/introduction.adoc new file mode 100644 index 00000000000..b013211d760 --- /dev/null +++ b/docs/src/modules/ROOT/pages/deploying-to-platform/introduction.adoc @@ -0,0 +1,103 @@ +[#_deploying_to_platform_introduction] += Deploying to the Timefold Platform (Preview) +:description: Why deploy your service model to Timefold Platform. +:doctype: book +:icons: font + +include::../_attributes.adoc[] + +Building a model is only half of getting a planning problem solved in production. +The other half is operational: hosting it, scaling it, exposing it to consumers through an API, and making its decisions explainable. + +Timefold Platform takes care of that other half, which is why deploying your model there is the natural next step once it's built. + +include::_preview-note.adoc[] + +NOTE: Using Timefold Platform, including deploying and running your own custom model, is subject to usage fees. + +== Platform layers + +The platform is organized in layers, each building on the one beneath it, per the platform's own https://docs.timefold.ai/timefold-platform/latest/introduction[introduction]. +From a model developer's perspective, each layer is plumbing you'd otherwise have to build yourself. + +[#_enterprise_edition_features] +=== Enterprise Edition features + +These Timefold Solver Enterprise Edition capabilities are especially valuable once your model is deployed, since consumers interact with them directly through the platform UI and API. +Since deploying to the platform requires building your model with the Enterprise Edition, as described in xref:deploying-to-platform/getting-started.adoc#_enterprise_edition[Build with the Enterprise Edition], you get these for free. + +* *Explainability*: xref:constraints-and-score/understanding-the-score.adoc[Score analysis] breaks a solution down constraint-by-constraint, so consumers can see why the solver made the decisions it did, and what the impact would be of overriding one. +* *Recommendations*: the xref:responding-to-change/recommendation-api.adoc#assignmentRecommendationAPI[Recommendation API] suggests the best fit for a new assignment without a full re-solve. +* *Performance*: xref:running-timefold-solver/multithreaded-solving.adoc#multithreadedIncrementalSolving[multithreaded solving], xref:optimization-algorithms/move-selector-reference.adoc#nearbySelection[nearby selection], and xref:constraints-and-score/performance.adoc#constraintProfiling[constraint profiling] let the solver scale to larger datasets. + +[#_hosting_and_orchestration] +=== Hosting and orchestration + +* *Hosting and scaling*: the platform builds, stores, and runs the container image, scaling it to the number of concurrent solve requests, so you're not managing a Kubernetes deployment or a fleet of containers. +* *Compliance*: the platform is ISO and SOC certified, so you don't have to pursue and maintain those certifications yourself for your model's hosting. +See https://trust.timefold.ai/[trust.timefold.ai] for details. +* *A multi-tenant REST gateway*: consumers of your model authenticate with API keys scoped to a tenant, so authentication, rate limiting, and per-customer isolation come built in. +* *Role-based access control*: https://docs.timefold.ai/timefold-platform/latest/how-tos/member-management-and-roles[member management and roles] gives tenants a built-in User/Administrator permission model, distinguishing who can view datasets from who can manage your model's configuration profiles, API keys, and members, so you don't have to build your own authorization layer. +* *Solve queue*: when concurrent solve requests exceed provisioned resources, the https://docs.timefold.ai/timefold-platform/latest/how-tos/solve-queue[solve queue] automatically queues and runs them as capacity frees up, so you don't have to implement your own request throttling or backpressure. +* *Audit logging*: configuration changes to your model, such as updates to a configuration profile, are automatically recorded in the https://docs.timefold.ai/timefold-platform/latest/how-tos/audit-log[audit log], with who made the change and when, without you building your own change-tracking system. + +[#_integrations] +=== Integrations + +* *Result delivery*: consumers receive results via https://docs.timefold.ai/timefold-platform/latest/api/receiving-model-api-results/receiving-model-api-results[webhooks, server-sent events, or polling], so you don't have to design your own asynchronous job-notification mechanism. +* *Self-service configuration profiles*: consumers can add their own configuration profiles (termination limits, thread counts, constraint weight overrides) without opening a pull request against your model. +* *Parallel versions and staggered releases*: because a model's identifier includes its API version, you can run multiple major versions of your model side by side and let consumers migrate at their own pace. +Within a version, you can stagger a release the same way https://docs.timefold.ai/timefold-platform/latest/models/versioning-and-maturity#_staggered_model_releases[Timefold does for its own models]: register a new implementation under its own registration key, restricted to a subset of tenants (for example a staging tenant), while other tenants keep running the previous implementation. +See xref:#_registration_key[Registration key] for how registration keys make this possible. +* *Incremental replanning*: the https://docs.timefold.ai/timefold-platform/latest/how-tos/from-patch-endpoint[`/from-patch` endpoint] lets consumers apply small, targeted changes to an existing dataset instead of resubmitting the full input, with built-in traceability and versioning between revisions, so you don't have to design your own patch protocol or dataset lineage tracking. +* *Secrets management*: if your model calls out to external systems, for example through webhooks, https://docs.timefold.ai/timefold-platform/latest/how-tos/secrets-management[secrets management] lets consumers store credentials encrypted and write-only, instead of you building your own credential storage. +* *Maps service*: for routing models, the https://docs.timefold.ai/timefold-platform/latest/how-tos/maps-service[maps service] calculates and caches distance and travel matrices for you, with incremental updates, throttling, and concurrency guarding against the map provider, instead of you integrating and managing a map provider yourself. +It supports multiple providers out of the box, and consumers can configure which one to use through your model's xref:deploying-to-platform/model-metadata.adoc#_configuration_profiles[configuration profile]. + +[#_explainability_and_trust] +=== Explainability and trust + +* *A UI for your Model API consumers*: the platform UI lets consumers submit datasets, inspect the resulting plan, and read a constraint-by-constraint score analysis, without you building any of that tooling yourself. +* *Dataset organization*: consumers can name, tag, search, and filter their datasets out of the box, as described in https://docs.timefold.ai/timefold-platform/latest/how-tos/categorizing-runs[categorizing datasets], instead of you building dataset management UI. +* *Dataset comparison*: https://docs.timefold.ai/timefold-platform/latest/how-tos/comparing-runs[comparing datasets] lets consumers compare metrics, scores, and constraint breakdowns across multiple datasets side by side, including radar charts and saved comparisons, without you building any comparison tooling. + +[#_intelligence] +=== Intelligence + +* *Insights*: https://docs.timefold.ai/timefold-platform/latest/how-tos/insights[Insights] tracks metric trends across datasets over time, so consumers can spot whether operational planning is improving or degrading, without you building your own analytics dashboard. +* *Experiments*: https://docs.timefold.ai/timefold-platform/latest/how-tos/experiments[experiments] let consumers run many dataset and configuration-profile combinations in one batch and compare the results, including benchmarking different model versions against each other, instead of you building your own scenario-testing or benchmarking harness. + +In short, deploying to the platform turns your model from "a service I have to run" into "a product other people can safely self-serve." + +[#_platform_concepts] +== Platform concepts + +A few terms come up throughout this section of the documentation: + +Custom model:: The model you build and deploy yourself, as opposed to one of Timefold's https://docs.timefold.ai/[pre-built, off-the-shelf models]. +This is what this section of the documentation is about. + +Model registration:: The act of publishing your model's descriptor and container image to the platform under a registration key, using `timefold:deploy`. +See xref:deploying-to-platform/getting-started.adoc[Getting started: deploying to the platform] for the full walkthrough. + +[#_model_identifier] +Model identifier:: Made up of your model's bare id and its API version, combined as `_` (for example `schooltimetabling_v1`). +This is what appears in the consumer-facing API path (`/api/models//`). The version comes from `timefold.application.version`. + +[#_registration_key] +Registration key:: The `key` you set through the Maven plugin, as described in xref:deploying-to-platform/getting-started.adoc#_add_the_maven_plugin[Add the Maven plugin]. +This is an administrative identifier, used only for managing the deployment itself (updating, overwriting, or undeploying it) and isn't part of the consumer-facing API. +Keeping it distinct from the model identifier matters once you deploy more than one implementation of the same model, for example a stable version and a private beta of the next version: both can share the same model identifier and API path, while each has its own registration key for independent management. + +[#_model_privacy] +Model privacy:: Every registered model has a visibility scope that determines which tenants can see and use it: *Private* (a single tenant) or *Shared* (a selected set of tenants). +The Maven plugin's `deploy` goal infers one of these automatically from how many tenants you list, as described in xref:deploying-to-platform/getting-started.adoc#_add_the_maven_plugin[Add the Maven plugin]. +A third scope, *Public* (visible to every tenant on the platform), also exists, but registering a model as Public is restricted to global admins of the platform, so it isn't a self-service option for model developers. + +Model consumers:: The tenant users, and the systems they build, that call your model's REST API: submitting datasets, reading results and score analysis, and managing configuration profiles. +As the model developer, you typically aren't the consumer yourself; you're building something others on your tenant (or on tenants you share the model with) will use. + +Model subscription:: Tenants subscribe to a model by its model identifier (not its registration key) to get access to it. +The Maven plugin can automate this for you: its `handleSubscription` setting subscribes your tenant when you register the model, and unsubscribes it when you undeploy. + +To get started, see xref:deploying-to-platform/getting-started.adoc[Getting started: deploying to the platform]. diff --git a/docs/src/modules/ROOT/pages/deploying-to-platform/metrics.adoc b/docs/src/modules/ROOT/pages/deploying-to-platform/metrics.adoc new file mode 100644 index 00000000000..dc5a69f21b6 --- /dev/null +++ b/docs/src/modules/ROOT/pages/deploying-to-platform/metrics.adoc @@ -0,0 +1,29 @@ +[#_using_metrics] += Using metrics +:description: Input and output metrics make platform features like solve graphs, comparison, and Insights more useful. +:doctype: book +:icons: font + +include::../_attributes.adoc[] + +include::_preview-note.adoc[] + +Metrics aren't just extra fields in the API response. +Once your model is xref:deploying-to-platform/getting-started.adoc[deployed to Timefold Platform], input and output metrics feed several platform features that would otherwise only show consumers a raw score and a solved dataset: + +* *Solve graphs*: output metrics can be plotted alongside the hard, medium, and soft score as the solver runs, so consumers see how a metric evolves over time, not just its final value. +See https://docs.timefold.ai/timefold-platform/latest/how-tos/interpreting-model-run-results#_solve_graphs[Solve graphs] for details. +* *Dataset comparison*: input and output metrics are available as columns when comparing multiple datasets side by side, making it possible to spot how problem size or solution quality differs between them. +See https://docs.timefold.ai/timefold-platform/latest/how-tos/comparing-runs[Comparing datasets] for details. +* *Insights*: both metric types can be tracked over time across many datasets, so consumers can tell whether operational planning is improving or degrading. +See https://docs.timefold.ai/timefold-platform/latest/how-tos/insights[Insights] for details. + +Without input and output metrics, none of these views have anything model-specific to show beyond the score. + +See xref:running-timefold-solver/service/exposing-metrics.adoc[Exposing metrics] for how to define and implement input and output metrics on your model. + +While you're at it, give each metric a proper OpenAPI `title`, `description`, and `format` in its `@Schema` annotation. +These surface directly in the platform UI: in solve graphs, the comparison view, and Insights, consumers see the `title` as the metric's label and the `description` as its explanation, not the raw field name. +A metric without a clear title and description is much harder for a consumer to interpret at a glance. + +Redeploy your model, as described in xref:deploying-to-platform/getting-started.adoc[Getting started: deploying to the platform], then submit a dataset and check that your metrics show up in the solve graphs, comparison view, and Insights. diff --git a/docs/src/modules/ROOT/pages/deploying-to-platform/model-metadata.adoc b/docs/src/modules/ROOT/pages/deploying-to-platform/model-metadata.adoc new file mode 100644 index 00000000000..eab0a9f6c96 --- /dev/null +++ b/docs/src/modules/ROOT/pages/deploying-to-platform/model-metadata.adoc @@ -0,0 +1,198 @@ +[#_platform_model_metadata] += Platform model metadata +:description: Platform-specific model metadata that shapes the score analysis, configuration profile, and model identity UI. +:doctype: book +:sectnums: +:icons: font + +Once your model is xref:deploying-to-platform/getting-started.adoc[deployed to Timefold Platform], the platform UI surfaces several pieces of your model's code and configuration directly. + +This page documents that platform-specific metadata: which identity properties describe your model on the platform, how to give your constraints human-readable names and groups, and how to ship a default configuration profile. + +See xref:deploying-to-platform/introduction.adoc[Deploying to the Timefold Platform] for why you'd want to do this in the first place, and xref:deploying-to-platform/introduction.adoc#_platform_concepts[Platform concepts] for how this metadata relates to your model's identifier, registration key, and visibility on the platform. + +include::_preview-note.adoc[] + +[#_model_identity_properties] +== Model identity properties + +The following `application.properties` describe your model's identity on the platform. +`timefold.application.name`, `timefold.application.version`, and the `timefold.application.contact.*` properties are covered in xref:quickstart/service/getting-started.adoc[Getting started: building a service], since they are required for every model, service or not. + +[cols="2,3", options="header"] +|=== +| Property | Purpose + +| `timefold.application.description` +| Description of the model, included in the generated OpenAPI specification and shown on the platform. + +| `timefold.model.maturity-level` +| Maturity level of the model. Defaults to `Experimental` if not set. +|=== + +Use `timefold.model.maturity-level` to tell consumers how stable a given model is, using the same https://docs.timefold.ai/timefold-platform/latest/models/versioning-and-maturity[maturity levels] Timefold uses for its own models (`Example`, `Experimental`, `Preview`, `Stable`, `Deprecated`). +This is especially useful once you're xref:deploying-to-platform/introduction.adoc#_integrations[running multiple versions of your model in parallel]: consumers can tell at a glance which version is safe to build on and which is still being validated, instead of having to ask you. + +=== Logo + +To give your model a custom logo on the platform, add a square `logo.png` file to a `model-images` folder at the root of your project, next to `pom.xml`: + +[source,options="nowrap"] +---- +my-model/ +├── model-images/ +│ └── logo.png +├── src/ +└── pom.xml +---- + +The build picks up any file in `model-images` automatically; `logo.png` specifically is used as the model's logo, and any other file is added as an additional model image. + +[#_constraint_descriptions_and_groups] +== Constraint descriptions and groups + +By default, a constraint is identified only by the string you pass to `asConstraint(String id)`, which is also used as its display name. +To give consumers a better score analysis and configuration profile experience, attach a `ConstraintInfo` to the constraint instead. + +`ConstraintInfo` and `ConstraintGroupInfo` live in `ai.timefold.solver.service.definition.api.description`: + +* `ConstraintInfo(id, name, description, constraintGroup)`: `id` is the stable identifier of the constraint, `name` is the human-readable name shown in the UI, `description` explains the constraint's goal, and `constraintGroup` optionally assigns the constraint to a `ConstraintGroupInfo`. +* `ConstraintGroupInfo(id, name, description, icon, tags)`: groups related constraints under a shared category. `icon` accepts any icon name from https://tabler.io/icons[Tabler Icons], and `tags` are optional labels you can use to classify or filter groups. + +Extending the `TimetableConstraintProvider` from xref:running-timefold-solver/service/constraint-overrides.adoc[Adjusting constraint weights] with names, descriptions, and a group: + +.The ConstraintProvider class, with ConstraintInfo attached. +[tabs] +==== +Java:: ++ +-- +[source,java,options="nowrap"] +---- +public class TimetableConstraintProvider implements ConstraintProvider { + + public static final String TEACHER_CONFLICT = "Teacher conflict"; + public static final String ROOM_CONFLICT = "Room conflict"; + + private static final ConstraintGroupInfo CONFLICT_GROUP = new ConstraintGroupInfo( + "conflicts", + "Conflicts", + "Constraints that prevent double-booking of teachers and rooms.", + "alert-triangle", + new String[] { "hard-constraints" }); + + Constraint roomConflict(ConstraintFactory constraintFactory) { + return constraintFactory + // constraint implementation excluded + .asConstraint(new ConstraintInfo(ROOM_CONFLICT, "Room conflict", + "A room can be used for at most one lesson at the same time.", CONFLICT_GROUP)); + } + + Constraint teacherConflict(ConstraintFactory constraintFactory) { + return constraintFactory + // constraint implementation excluded + .asConstraint(new ConstraintInfo(TEACHER_CONFLICT, "Teacher conflict", + "A teacher can teach at most one lesson at the same time.", CONFLICT_GROUP)); + } + + // other constraints excluded +} +---- +-- + +Kotlin:: ++ +-- +[source,kotlin,options="nowrap"] +---- +class TimetableConstraintProvider : ConstraintProvider { + + companion object { + const val TEACHER_CONFLICT = "Teacher conflict" + const val ROOM_CONFLICT = "Room conflict" + + private val CONFLICT_GROUP = ConstraintGroupInfo( + "conflicts", + "Conflicts", + "Constraints that prevent double-booking of teachers and rooms.", + "alert-triangle", + arrayOf("hard-constraints") + ) + } + + fun roomConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(ConstraintInfo(ROOM_CONFLICT, "Room conflict", + "A room can be used for at most one lesson at the same time.", CONFLICT_GROUP)) + } + + fun teacherConflict(constraintFactory: ConstraintFactory): Constraint { + return constraintFactory + // constraint implementation excluded + .asConstraint(ConstraintInfo(TEACHER_CONFLICT, "Teacher conflict", + "A teacher can teach at most one lesson at the same time.", CONFLICT_GROUP)) + } + + // other constraints excluded +} +---- +-- +==== + +The `id` you pass into `ConstraintInfo` is still the identifier used by `@ConstraintReference` in your `ModelConfigOverrides`, as described in xref:running-timefold-solver/service/constraint-overrides.adoc[Adjusting constraint weights]. +The `name`, `description`, and `constraintGroup` are purely presentational: they are what the platform's score analysis view and configuration profile editor use to show your constraints to consumers, instead of falling back to the raw id. + +The UI groups constraints that don't specify a `constraintGroup` under a default group. + +[#_configuration_profiles] +== Configuration profiles + +Every model ships with a default configuration profile, defined through `ai.timefold.model.default-config.*` properties in `application.properties`. +This profile is what consumers get out of the box, before they add any profile of their own. + +[cols="2,3", options="header"] +|=== +| Property | Purpose + +| `ai.timefold.model.default-config.name` +| Name of the default configuration profile. + +| `ai.timefold.model.default-config.description` +| Description of the default configuration profile. + +| `ai.timefold.model.default-config.max-thread-count` +| Default number of threads used for solving. + +| `ai.timefold.model.default-config.map.provider` +| Default map provider, for models that require map data. + +| `ai.timefold.model.default-config.map.location` +| Default map location, for models that require map data. + +| `ai.timefold.model.default-config.map.max-distance-from-road` +| Default maximum distance from a road, for models that require map data. + +| `ai.timefold.model.default-config.map.transport-type` +| Default transport type used when computing distances, for models that require map data. + +| `ai.timefold.model.default-config.map.use-traffic` +| Whether the default profile takes live traffic into account. Defaults to `false`. + +| `ai.timefold.model.default-config.termination.spent-limit` +| The default maximum solving time, in the https://www.digi.com/resources/documentation/digidocs/90001488-13/reference/r_iso_8601_duration_format.htm[ISO 8601 duration format]. This property is *required*. + +| `ai.timefold.model.default-config.termination.unimproved-spent-limit` +| The default unimproved time limit. If not set, https://docs.timefold.ai/timefold-solver/latest/optimization-algorithms/overview#diminishedReturnsTermination[Diminished Returns termination] is used instead. +|=== + +Once your model is deployed, tenant users can add additional configuration profiles on top of this default directly in the platform UI, without you making any code changes. +A configuration profile is where per-request xref:running-timefold-solver/service/constraint-overrides.adoc[constraint weight overrides], thread count, memory, and termination limits are set for a specific use case. +See https://docs.timefold.ai/timefold-platform/latest/how-tos/configuration-parameters-and-profiles[Configuration parameters and profiles] for how tenant users manage profiles from the platform side. + +[#_visualization] +== Visualization + +There is currently no dedicated mechanism for adding a custom visualization UI for your model's solution on the platform. +Consumers see the raw solution data and the generic score analysis view. +This is an open area of the platform, so expect it to evolve in future releases. diff --git a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc index da43c65083d..7487fb801d4 100644 --- a/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/service/getting-started.adoc @@ -477,4 +477,6 @@ Since this is a "Getting Started" guide, not everything is covered yet. * Learn about improvements you can make to your model: ** How to enrich your model with xref:running-timefold-solver/service/modeling-changes.adoc[Model Enrichment]. -** How to configure your xref:running-timefold-solver/service/rest-api.adoc[REST API] with validations, custom endpoints, etc. \ No newline at end of file +** How to configure your xref:running-timefold-solver/service/rest-api.adoc[REST API] with validations, custom endpoints, etc. +** How to xref:deploying-to-platform/getting-started.adoc[deploy this model to Timefold Platform]. +** How to xref:deploying-to-platform/metrics.adoc[use input and output metrics] to make platform features like solve graphs, comparison, and Insights more useful. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/constraint-overrides.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/constraint-overrides.adoc index 5737aa42196..cf8162f8b94 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/constraint-overrides.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/constraint-overrides.adoc @@ -161,6 +161,14 @@ Next, in the xref:running-timefold-solver/service/rest-api.adoc#modelConverter[m to a `ConstraintWeightOverrides` object and set it on the `@PlanningSolution` class xref:#enablingWeightOverrides[as described above]: +[IMPORTANT] +==== +This requires a custom `ModelConvertor`. +If your `SolverModel` also serves as `ModelInput` and `ModelOutput`, for example because you extend `AbstractSimpleModel`, the framework uses a trivial `ModelConvertor` by default, which ignores `modelConfig` entirely. +With only the trivial convertor in place, constraint weight overrides submitted in a request are silently dropped: the request succeeds, but the override never reaches the solver. +Provide your own `ModelConvertor` implementation, as shown below, to actually apply them. +==== + .As part of the ModelConverter. [tabs] ==== @@ -171,12 +179,12 @@ Java:: ---- TimetableConfigOverrides modelConfigOverrides = modelConfig.overrides(); -ConstraintWeightOverrides constraintWeightOverrides = ConstraintWeightOverrides.of( +ConstraintWeightOverrides constraintWeightOverrides = ConstraintWeightOverrides.of( Map.ofEntries( Map.entry(TimetableConstraintProvider.TEACHER_CONFLICT, - HardMediumSoftLongScore.ofHard(modelConfigOverrides.getTeacherConflictWeight())), + HardMediumSoftScore.ofHard(modelConfigOverrides.getTeacherConflictWeight())), Map.entry(TimetableConstraintProvider.ROOM_CONFLICT, - HardMediumSoftLongScore.ofSoft(modelConfigOverrides.getRoomConflictWeight())) + HardMediumSoftScore.ofSoft(modelConfigOverrides.getRoomConflictWeight())) ) ); @@ -194,9 +202,9 @@ val modelConfigOverrides = modelConfig.overrides() val constraintWeightOverrides = ConstraintWeightOverrides.of( mapOf( TimetableConstraintProvider.TEACHER_CONFLICT to - HardMediumSoftLongScore.ofHard(modelConfigOverrides.teacherConflictWeight), + HardMediumSoftScore.ofHard(modelConfigOverrides.teacherConflictWeight), TimetableConstraintProvider.ROOM_CONFLICT to - HardMediumSoftLongScore.ofSoft(modelConfigOverrides.roomConflictWeight) + HardMediumSoftScore.ofSoft(modelConfigOverrides.roomConflictWeight) ) ) diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/demo-data.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/demo-data.adoc index 8d915ce2fdf..0bc28e66d37 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/demo-data.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/demo-data.adoc @@ -133,7 +133,9 @@ class TimetableDemoDataGenerator : DemoDataGenerator { -- ==== -With this interface implemented, Timefold Solver will automatically expose these methods as REST endpoints: +`ModelRequest` has two constructors: `ModelRequest(ModelInput_ modelInput)`, which defaults the configuration to null, and `ModelRequest(Configuration configuration, ModelInput_ modelInput)`, if you also want to set a non-default configuration. +There is no constructor that takes the model config overrides directly; overrides are wrapped in a `Configuration` object first. -- `GET /<root>/demo-data`: Retrieve all available demo dataset ids. -- `GET /<root>/demo-data/\{demoDataId\}`: Retrieve the demo dataset with the given identifier +With this interface implemented, Timefold Solver will automatically expose these methods as REST endpoints. +These endpoints are *not* nested under your `ModelRest` resource path: they only share its version prefix. +See xref:./rest-api.adoc#generatedEndpoints[Generated endpoints] for the exact path. diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/exposing-metrics.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/exposing-metrics.adoc index 634049b731e..606419bf197 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/exposing-metrics.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/exposing-metrics.adoc @@ -141,6 +141,12 @@ Like input metrics, they are exposed through the xref:./rest-api.adoc[REST API] It is therefore necessary to add xref:./rest-api.adoc#openAPISpecification[OpenAPI Specification] annotations to the fields. ==== +[WARNING] +==== +If a metric is a timestamp, use `java.time.OffsetDateTime` or `java.time.Instant`, not `java.time.LocalDateTime`. +The platform's `date-time` format validation expects a timezone offset (for example `2027-02-02T09:00:00Z`); `LocalDateTime` serializes without one and fails validation. +==== + .Example for School Timetabling [tabs] ==== diff --git a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc index 39827767be3..7ee13460ad5 100644 --- a/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc +++ b/docs/src/modules/ROOT/pages/running-timefold-solver/service/rest-api.adoc @@ -87,9 +87,12 @@ The `` is determined by the @Path annotation on the interface extending Mo - `DELETE //\{id\}`: Terminate a dataset optimization run If your model provides xref:./demo-data.adoc[demo data], the following endpoints are also created. +These are *not* nested under ``: they only share its version prefix (for example `/v1`), not the rest of the path. +So for a `@Path("/v1/timetables")` resource, the demo data endpoints are at `/v1/demo-data`, not `/v1/timetables/demo-data`. +If your resource path has no version prefix, the demo data endpoints are served at the root, without a version. -- `GET //demo-data`: Retrieve all available demo dataset ids. -- `GET //demo-data/\{demoDataId\}`: Retrieve the demo dataset with the given identifier +- `GET /v/demo-data`: Retrieve all available demo dataset ids. +- `GET /v/demo-data/\{demoDataId\}`: Retrieve the demo dataset with the given identifier // TODO See the xref:consumer-guide.adoc[service consumer guide] for information on how consumers of the service should interact with these API endpoints. // TODO https://github.com/TimefoldAI/timefold-solver/issues/2349 @@ -166,6 +169,13 @@ Additionally, more fields might be added for specific model implementations: - `inputMetrics`: metrics about the input of the planning problem. See: xref:./exposing-metrics.adoc#modelInputMetrics[Input Metrics] - `kpis`: metrics about the output of the planning problem. See: xref:./exposing-metrics.adoc#modelOutputMetrics[Output Metrics] +[NOTE] +==== +This envelope is represented in Java/Kotlin by the `ModelResponse` record (`ai.timefold.solver.service.definition.api.domain.ModelResponse`). +Its accessor is named `outputMetrics()`, not `kpis()`; only the JSON serialization uses the `kpis` key (via `@JsonProperty("kpis")`). +The score itself is not a top-level field: it's nested inside `metadata.score`, not `metadata.run.score` or a top-level `score`. +==== + .Example Response [source,json] ---- @@ -236,7 +246,7 @@ Java:: [source,java,options="nowrap"] ---- @ApplicationScoped -public class TimetableConvertor implements ModelConvertor { +public class TimetableConvertor implements ModelConvertor { @Override public Timetable toSolverModel(TimetableDto modelInput, ModelConfig modelConfig, @@ -263,7 +273,7 @@ Kotlin:: [source,kotlin,options="nowrap"] ---- @ApplicationScoped -class TimetableConvertor : ModelConvertor { +class TimetableConvertor : ModelConvertor { override fun toSolverModel(modelInput: TimetableDto, modelConfig: ModelConfig, lastModelOutput: Optional): Timetable { diff --git a/service/tools/maven-plugin/README.adoc b/service/tools/maven-plugin/README.adoc index 49112f3a633..ef69cda831e 100644 --- a/service/tools/maven-plugin/README.adoc +++ b/service/tools/maven-plugin/README.adoc @@ -85,7 +85,8 @@ The plugin uses the JDK HttpClient API (java.net.http.HttpClient) to perform net == Example usage (pom) -Add plugin configuration in your project's POM (example snippet): +Add plugin configuration in your project's POM (example snippet). +Bind `timefold:configure` to the `initialize` phase via `` so it runs automatically as part of `mvn package`. (It only actually does work when `timefold:deploy` is also among the requested goals, so this binding is safe to keep in place for regular builds.) [source,xml] ---- @@ -94,7 +95,16 @@ Add plugin configuration in your project's POM (example snippet): ai.timefold.solver timefold-maven-plugin - PUT_PLUGIN_VERSION_HERE + ${project.parent.version} + + + configure + initialize + + configure + + + https://api.timefold.example my-model-key @@ -109,6 +119,8 @@ Add plugin configuration in your project's POM (example snippet): ---- +With this binding in place, `mvn clean package timefold:deploy` runs `configure` during the `initialize` phase (before the container image is built later in `package`). Without it, `configure` never runs unless invoked explicitly, and the container image build won't have the platform's required registry/group configuration. + Or call the plugin directly from CLI with system properties and environment var: [source,bash]