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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ reviews:

- path: "core/src/main/resources/db/migration/*.sql"
instructions: |
Flyway migrations are immutable after release. Check tenant isolation,
foreign keys, uniqueness, indexes, append-only evidence semantics, safe
defaults, and compatibility with PostgreSQL 18 plus pgvector.
The repository is pre-release: V1 is the intentionally resettable clean
baseline and development data carries no migration cost. Once a release
baseline is frozen, later Flyway migrations are immutable. Check tenant
isolation, foreign keys, uniqueness, indexes, append-only evidence
semantics, safe defaults, and PostgreSQL 18 plus pgvector compatibility.

- path: "integrations/authorization-openfga/**/*"
instructions: |
Expand All @@ -77,10 +79,10 @@ reviews:

- path: ".github/**/*.{yml,yaml}"
instructions: |
Require least-privilege permissions, immutable action SHAs, bounded job
timeouts, concurrency cancellation, frozen lockfiles, and no secrets in
pull-request workflows. Keep Dependabot comments beside pinned action
SHAs so action updates remain discoverable.
Require least-privilege permissions, explicit release tags for actions,
bounded job timeouts, concurrency cancellation, frozen lockfiles, and no
secrets in pull-request workflows. GitHub Actions are intentionally not
pinned to commit SHAs; Dependabot owns their scheduled version updates.

tools:
github-checks:
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ jobs:
- name: Build production bundle
run: pnpm build

- name: Install Playwright browser
run: pnpm exec playwright install --with-deps chromium

- name: Run browser tests
run: pnpm test:e2e

- name: Check generated route tree
run: git diff --exit-code -- src/routeTree.gen.ts

Expand All @@ -108,13 +114,47 @@ jobs:
working-directory: integrations/authorization-openfga/src/test/openfga
run: fga model test --tests store.fga.yaml

evaluation:
name: Evaluation · Python 3.13
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: evaluation
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
Comment thread
kl3inIT marked this conversation as resolved.
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v6.2.0
with:
python-version: "3.13"

- name: Set up uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: evaluation/uv.lock

- name: Install locked evaluation environment
run: uv sync --frozen --dev

- name: Lint evaluation tooling
run: uv run ruff check .

- name: Test deterministic evaluation contracts
run: uv run pytest

gate:
name: CI Gate
if: always()
needs:
- backend
- web
- openfga
- evaluation
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -124,8 +164,10 @@ jobs:
BACKEND_RESULT: ${{ needs.backend.result }}
WEB_RESULT: ${{ needs.web.result }}
OPENFGA_RESULT: ${{ needs.openfga.result }}
EVALUATION_RESULT: ${{ needs.evaluation.result }}
run: |
set -euo pipefail
[[ "$BACKEND_RESULT" == "success" ]]
[[ "$WEB_RESULT" == "success" ]]
[[ "$OPENFGA_RESULT" == "success" ]]
[[ "$EVALUATION_RESULT" == "success" ]]
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ tmp/
output/
test-results/
.tools/
.venv/
__pycache__/
*.py[cod]

# --- OS ---
.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions apps/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
implementation(project(":integrations:authorization-openfga"))
implementation(project(":integrations:connectors"))
implementation(project(":integrations:graph-rag-postgres"))
implementation(project(":integrations:graph-rag-observability"))
implementation(project(":integrations:graph-rag-spring-ai"))
implementation(project(":integrations:object-storage-minio"))

Expand All @@ -18,6 +19,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-session-jdbc")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-opentelemetry")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")
implementation(libs.springdoc.webmvc)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.orgmemory.api;

import com.orgmemory.core.knowledge.GraphExplorerProperties;
import com.orgmemory.core.knowledge.GraphProcessingProperties;
import com.orgmemory.core.knowledge.KnowledgeEmbeddingProperties;
import com.orgmemory.core.knowledge.KnowledgeRetrievalProperties;
import com.orgmemory.core.knowledge.SourceIngestionProperties;
Expand All @@ -24,7 +25,8 @@
SecretCipherProperties.class,
KnowledgeRetrievalProperties.class,
KnowledgeEmbeddingProperties.class,
GraphExplorerProperties.class
GraphExplorerProperties.class,
GraphProcessingProperties.class
})
public class OrgMemoryApiApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.orgmemory.api.assistant;

import com.orgmemory.api.security.CurrentActorProvider;
import com.orgmemory.core.assistant.AssistantCitation;
import com.orgmemory.core.assistant.AssistantService;
import com.orgmemory.core.assistant.AssistantTurn;
import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence;
import com.orgmemory.core.organization.CurrentActor;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
Expand Down Expand Up @@ -83,20 +83,25 @@ Flux<AssistantStreamPart> parts(AssistantTurn turn) {
: streamedTokens);
return Flux.concat(
Flux.just(new AssistantStreamPart.StartStep()),
Flux.fromIterable(turn.evidence())
Flux.fromIterable(turn.citations())
.map(AssistantController::sourcePart),
text,
Flux.just(new AssistantStreamPart.FinishStep()));
}

private static AssistantStreamPart sourcePart(RetrievedKnowledgeEvidence evidence) {
String sourceId = "urn:orgmemory:citation:" + evidence.chunkId();
private static AssistantStreamPart sourcePart(AssistantCitation citation) {
var evidence = citation.evidence();
String sourceId = "urn:orgmemory:citation:"
+ citation.number()
+ ":"
+ evidence.chunkId();
String title = evidence.startPage() == null
? evidence.title()
: evidence.title() + " · page " + evidence.startPage();
return new AssistantStreamPart.SourceUrl(
sourceId,
"/api/citations/" + evidence.chunkId() + "/content",
title);
title,
citation.number());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ record TextDelta(String id, String delta) implements AssistantStreamPart {
record TextEnd(String id) implements AssistantStreamPart {
}

record SourceUrl(String sourceId, String url, String title) implements AssistantStreamPart {
record SourceUrl(
String sourceId,
String url,
String title,
int citationNumber) implements AssistantStreamPart {
}

record SourceDocument(String sourceId, String mediaType, String title, String filename)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ private static Map<String, Object> payload(AssistantStreamPart part) {
"type", "source-url",
"sourceId", source.sourceId(),
"url", source.url(),
"title", source.title());
"title", source.title(),
"providerMetadata", fields(
"orgmemory", fields(
"citationNumber",
source.citationNumber())));
case AssistantStreamPart.SourceDocument source -> fields(
"type", "source-document",
"sourceId", source.sourceId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.orgmemory.api.security.CurrentActorProvider;
import com.orgmemory.core.knowledge.CitationContentService;
import com.orgmemory.core.knowledge.KnowledgeContentType;
import io.swagger.v3.oas.annotations.Operation;
import java.util.UUID;
import org.springframework.http.CacheControl;
Expand Down Expand Up @@ -47,14 +48,16 @@ ResponseEntity<StreamingResponseBody> content(
citation.stream().transferTo(output);
}
};
SafeRepresentation representation =
safeRepresentation(citation.fileName());
return ResponseEntity.ok()
.contentType(safeMediaType(citation.mediaType()))
.contentType(representation.mediaType())
.contentLength(citation.contentLength())
.cacheControl(CacheControl.noStore())
.header("X-Request-ID", requestId)
.header(
HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.inline()
representation.contentDisposition()
.filename(
citation.fileName(),
java.nio.charset.StandardCharsets.UTF_8)
Expand All @@ -64,11 +67,20 @@ ResponseEntity<StreamingResponseBody> content(
.body(body);
}

private static MediaType safeMediaType(String value) {
try {
return MediaType.parseMediaType(value);
} catch (IllegalArgumentException ignored) {
return MediaType.APPLICATION_OCTET_STREAM;
}
private static SafeRepresentation safeRepresentation(String fileName) {
return KnowledgeContentType.fromFileName(fileName)
.map(contentType -> new SafeRepresentation(
MediaType.parseMediaType(
contentType.browserSafeMediaType()),
contentType.inlinePreviewAllowed()
? ContentDisposition.inline()
: ContentDisposition.attachment()))
.orElseGet(() -> new SafeRepresentation(
MediaType.APPLICATION_OCTET_STREAM,
ContentDisposition.attachment()));
}

private record SafeRepresentation(
MediaType mediaType,
ContentDisposition.Builder contentDisposition) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,16 @@ GraphIndexJobView resumeGraphJob(
@PathVariable UUID jobId, Authentication authentication) {
return graphJobs.resume(actors.current(authentication), jobId);
}

@PostMapping("/{knowledgeAssetId}/graph-index")
@Operation(
operationId = "ensureKnowledgeAssetGraphIndex",
summary = "Ensure graph indexing uses the current processing profile")
@ResponseStatus(HttpStatus.ACCEPTED)
GraphIndexJobView ensureGraphIndex(
@PathVariable UUID knowledgeAssetId,
Authentication authentication) {
return graphJobs.ensureCurrentProfile(
actors.current(authentication), knowledgeAssetId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ SourceResponse upload(
new CreateUploadSourceCommand(
actor,
file.getOriginalFilename(),
file.getContentType(),
file.getSize(),
classification,
knowledgeSpaceId),
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ spring:
application:
name: orgmemory-api
ai:
chat:
observations:
log-prompt: false
log-completion: false
model:
chat: ${ORGMEMORY_AI_MODEL_CHAT:none}
embedding: ${ORGMEMORY_AI_MODEL_EMBEDDING:none}
Expand Down Expand Up @@ -90,6 +94,13 @@ orgmemory:
model: ${ORGMEMORY_OPENAI_EMBEDDING_MODEL:text-embedding-3-large}
dimensions: ${ORGMEMORY_OPENAI_EMBEDDING_DIMENSIONS:1536}
graph-rag:
processing:
maximum-entities-per-chunk: ${ORGMEMORY_GRAPH_MAX_ENTITIES_PER_CHUNK:40}
maximum-relations-per-chunk: ${ORGMEMORY_GRAPH_MAX_RELATIONS_PER_CHUNK:60}
entity-type-guidance: ${ORGMEMORY_GRAPH_ENTITY_TYPES:PERSON,ORGANIZATION,TEAM,ROLE,POLICY,PROCESS,SYSTEM,PRODUCT,DOCUMENT,LOCATION,EVENT,CONCEPT,OTHER}
maximum-gleaning-rounds: ${ORGMEMORY_GRAPH_MAX_GLEANING_ROUNDS:1}
maximum-gleaning-input-tokens: ${ORGMEMORY_GRAPH_MAX_GLEANING_INPUT_TOKENS:24000}
maximum-section-context-tokens: ${ORGMEMORY_GRAPH_MAX_SECTION_CONTEXT_TOKENS:256}
explorer:
default-entity-limit: ${ORGMEMORY_GRAPH_EXPLORER_DEFAULT_ENTITY_LIMIT:200}
maximum-entity-limit: ${ORGMEMORY_GRAPH_EXPLORER_MAXIMUM_ENTITY_LIMIT:1000}
Expand Down Expand Up @@ -136,6 +147,9 @@ server:
same-site: lax

management:
tracing:
sampling:
probability: ${ORGMEMORY_TRACING_SAMPLING_PROBABILITY:0.1}
endpoints:
web:
exposure:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.mockito.Mockito.mock;

import com.orgmemory.api.security.CurrentActorProvider;
import com.orgmemory.core.assistant.AssistantCitation;
import com.orgmemory.core.assistant.AssistantService;
import com.orgmemory.core.assistant.AssistantTurn;
import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence;
Expand All @@ -24,7 +25,7 @@ void streamsTheVerifiedRequestSnapshotWithoutWaitingForModelCompletion() {
Sinks.many().unicast().onBackpressureBuffer();
AssistantTurn turn = new AssistantTurn(
"request-1",
List.of(evidence),
List.of(new AssistantCitation(1, evidence)),
model.asFlux());

StepVerifier.create(controller().parts(turn))
Expand All @@ -40,6 +41,7 @@ void streamsTheVerifiedRequestSnapshotWithoutWaitingForModelCompletion() {
+ evidence.chunkId()
+ "/content",
source.url());
assertEquals(1, source.citationNumber());
})
.then(() -> model.tryEmitNext("answer"))
.assertNext(part -> assertInstanceOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ void emitsAiSdkUiMessageFramesInOrder() {
Flux.just(
new AssistantStreamPart.StartStep(),
new AssistantStreamPart.SourceUrl(
"citation-1", "https://example.test/handbook", "Employee Handbook"),
"citation-1",
"https://example.test/handbook",
"Employee Handbook",
1),
new AssistantStreamPart.TextStart("answer"),
new AssistantStreamPart.TextDelta("answer", "Sixty days. [1]"),
new AssistantStreamPart.TextEnd("answer"),
Expand All @@ -36,7 +39,7 @@ void emitsAiSdkUiMessageFramesInOrder() {
assertThat(data.getFirst()).contains("\"type\":\"start\"").contains("\"messageId\":");
assertThat(data.subList(1, data.size())).containsExactly(
"{\"type\":\"start-step\"}",
"{\"type\":\"source-url\",\"sourceId\":\"citation-1\",\"url\":\"https://example.test/handbook\",\"title\":\"Employee Handbook\"}",
"{\"type\":\"source-url\",\"sourceId\":\"citation-1\",\"url\":\"https://example.test/handbook\",\"title\":\"Employee Handbook\",\"providerMetadata\":{\"orgmemory\":{\"citationNumber\":1}}}",
"{\"type\":\"text-start\",\"id\":\"answer\"}",
"{\"type\":\"text-delta\",\"id\":\"answer\",\"delta\":\"Sixty days. [1]\"}",
"{\"type\":\"text-end\",\"id\":\"answer\"}",
Expand Down
Loading