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
26 changes: 15 additions & 11 deletions packages/api/src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ad4mModel, HasMany, HasManyMethods, Flag, Literal, LinkQuery, Model, Property, PerspectiveProxy, parseLit, parseSparqlCount, CountBinding } from '@coasys/ad4m';
import { Ad4mModel, HasMany, HasManyMethods, Flag, Literal, LinkQuery, Model, Property, PerspectiveProxy, parseSparqlCount, CountBinding } from '@coasys/ad4m';
import { community } from '@coasys/flux-constants';
import { EntryType } from '@coasys/flux-types';
import { SynergyGroup, SynergyItem, ItemType, icons } from '@coasys/flux-utils';
Expand Down Expand Up @@ -122,21 +122,23 @@ export class Channel extends Ad4mModel {
let type: ItemType = 'Message';
const itemType = binding.type;

// body/title/taskName/transcriptStart are stored as typed XSD
// literals; Oxigraph returns their lexical form directly, no decode.
if (itemType === 'flux://has_message') {
text = parseLit(binding.body);
text = binding.body ?? '';
type = 'Message';
} else if (itemType === 'flux://has_post') {
text = parseLit(binding.title);
text = binding.title ?? '';
type = 'Post';
} else if (itemType === 'flux://has_task') {
text = parseLit(binding.taskName);
text = binding.taskName ?? '';
type = 'Task';
}

return {
id: binding.id,
author: binding.author,
timestamp: new Date(parseLit(binding.transcriptStart) || binding.timestamp).toISOString(),
timestamp: new Date(binding.transcriptStart || binding.timestamp).toISOString(),
text,
type,
icon: icons[type] || 'question',
Expand Down Expand Up @@ -228,21 +230,22 @@ export class Channel extends Ad4mModel {
let type: ItemType = 'Message';
const itemType = binding.type;

// Typed XSD literals — no parseLit() decode needed.
if (itemType === 'flux://has_message') {
text = parseLit(binding.body);
text = binding.body ?? '';
type = 'Message';
} else if (itemType === 'flux://has_post') {
text = parseLit(binding.title);
text = binding.title ?? '';
type = 'Post';
} else if (itemType === 'flux://has_task') {
text = parseLit(binding.taskName);
text = binding.taskName ?? '';
type = 'Task';
}

return {
id: binding.id,
author: binding.author,
timestamp: new Date(parseLit(binding.transcriptStart) || binding.timestamp).toISOString(),
timestamp: new Date(binding.transcriptStart || binding.timestamp).toISOString(),
text,
type,
icon: icons[type] || 'question',
Expand Down Expand Up @@ -310,8 +313,9 @@ export class Channel extends Ad4mModel {
for (const r of results || []) {
const cid = r.channelId;
if (!cid || channelMap.has(cid)) continue;
const parsed = parseLit(r.isConv);
if (String(parsed) !== 'true') continue;
// CHANNEL_IS_CONVERSATION is a boolean typed literal; SPARQL binding
// returns its lexical form directly.
if (r.isConv !== 'true') continue;
channelMap.set(cid, {
channelId: cid,
conversationId: r.conversationId || undefined,
Expand Down
28 changes: 16 additions & 12 deletions packages/api/src/conversation-subgroup/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, Ad4mModel, Flag, HasMany, Property, Literal, parseLit } from '@coasys/ad4m';
import { Model, Ad4mModel, Flag, HasMany, Property, Literal } from '@coasys/ad4m';
import Topic, { TopicWithRelevance } from '../topic';
import SemanticRelationship from '../semantic-relationship';
import { SynergyTopic, SynergyItem, ItemType, icons } from '@coasys/flux-utils';
Expand Down Expand Up @@ -102,7 +102,8 @@ export default class ConversationSubgroup extends Ad4mModel {
if (topicBase && !uniqueTopics.has(topicBase)) {
uniqueTopics.set(topicBase, {
topicBase,
topicName: parseLit(binding.topicNameRaw),
// Typed XSD literal — SPARQL binding gives lexical form directly.
topicName: binding.topicNameRaw ?? '',
});
}
}
Expand Down Expand Up @@ -155,24 +156,26 @@ export default class ConversationSubgroup extends Ad4mModel {
if (!id) continue;

// Coalesce OPTIONAL fields from multiple SPARQL rows for same id
// body/title/taskName/transcriptStart are typed XSD literals; SPARQL
// binding returns their lexical form directly, no decode needed.
if (seen.has(id)) {
// Merge optional fields from this binding into the existing item
const existing = seen.get(id)!;
const transcriptStart = parseLit(binding.transcriptStart);
const transcriptStart = binding.transcriptStart ?? '';
const channelTs = binding.channelTs;
const fallbackTs = binding.timestamp;
if (!existing.channelTimestamp) {
existing.channelTimestamp = transcriptStart || channelTs || fallbackTs;
}
if (!existing.messageBody) existing.messageBody = parseLit(binding.body);
if (!existing.postTitle) existing.postTitle = parseLit(binding.title);
if (!existing.taskName) existing.taskName = parseLit(binding.taskName);
if (!existing.messageBody) existing.messageBody = binding.body ?? '';
if (!existing.postTitle) existing.postTitle = binding.title ?? '';
if (!existing.taskName) existing.taskName = binding.taskName ?? '';
if (!existing.type) existing.type = binding.type;
if (!existing.author) existing.author = binding.author;
continue;
}

const transcriptStart = parseLit(binding.transcriptStart);
const transcriptStart = binding.transcriptStart ?? '';
const channelTs = binding.channelTs;
const fallbackTs = binding.timestamp;
const channelTimestamp = transcriptStart || channelTs || fallbackTs;
Expand All @@ -182,9 +185,9 @@ export default class ConversationSubgroup extends Ad4mModel {
type: binding.type,
author: binding.author,
channelTimestamp,
messageBody: parseLit(binding.body),
postTitle: parseLit(binding.title),
taskName: parseLit(binding.taskName),
messageBody: binding.body ?? '',
postTitle: binding.title ?? '',
taskName: binding.taskName ?? '',
};
seen.set(id, item);
items.push(item);
Expand Down Expand Up @@ -249,8 +252,9 @@ export default class ConversationSubgroup extends Ad4mModel {
if (topicBase && !uniqueTopics.has(topicBase)) {
uniqueTopics.set(topicBase, {
topicBase,
topicName: parseLit(binding.topicNameRaw),
relevance: parseLit(binding.relevanceRaw),
// Typed XSD literals — no decode needed.
topicName: binding.topicNameRaw ?? '',
relevance: binding.relevanceRaw ?? '',
});
}
}
Expand Down
13 changes: 8 additions & 5 deletions packages/api/src/conversation/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ad4mModel, Ad4mClient, Flag, HasMany, HasManyMethods, Link, Literal, Model, Property, parseLit } from '@coasys/ad4m';
import { Ad4mModel, Ad4mClient, Flag, HasMany, HasManyMethods, Link, Literal, Model, Property } from '@coasys/ad4m';

import { getProfile, Topic } from '@coasys/flux-api';
import { ProcessingState, Profile } from '@coasys/flux-types';
Expand Down Expand Up @@ -112,7 +112,8 @@ export class Conversation extends Ad4mModel {
if (topicBase && !uniqueTopics.has(topicBase)) {
uniqueTopics.set(topicBase, {
topicBase,
topicName: parseLit(binding.topicNameRaw),
// Typed XSD literal — SPARQL binding gives lexical form directly.
topicName: binding.topicNameRaw ?? '',
});
}
}
Expand Down Expand Up @@ -164,8 +165,9 @@ export class Conversation extends Ad4mModel {
subgroupMap.set(id, {
id,
timestamp: binding.timestamp,
name: parseLit(binding.nameRaw),
summary: parseLit(binding.summaryRaw),
// has_name / has_summary are typed XSD literals — no decode.
name: binding.nameRaw ?? '',
summary: binding.summaryRaw ?? '',
});
}
}
Expand Down Expand Up @@ -196,7 +198,8 @@ export class Conversation extends Ad4mModel {
for (const r of batchResults || []) {
const sgId = r.sg;
if (!sgId) continue;
const ts = parseLit(r.transcriptStart) || r.channelTs;
// transcript_started_at is a typed XSD literal string.
const ts = r.transcriptStart || r.channelTs;
if (ts == null || ts === '') continue;
const time = new Date(ts).getTime();
if (isNaN(time)) continue;
Expand Down
13 changes: 8 additions & 5 deletions packages/api/src/semantic-relationship/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Model, Ad4mModel, Flag, Property } from '@coasys/ad4m';
import { parseLit } from '../utils/parseLit';
import { SynergyMatch } from '@coasys/flux-utils';

const TYPE_MAP: Record<string, string> = {
Expand Down Expand Up @@ -72,7 +71,8 @@ export default class SemanticRelationship extends Ad4mModel {
type: 'Conversation',
embedding: JSON.parse(embeddingExpression.data),
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
// has_channel_name is a typed XSD literal — no decode needed.
channelName: binding.channelName ?? '',
};
}),
);
Expand Down Expand Up @@ -110,7 +110,8 @@ export default class SemanticRelationship extends Ad4mModel {
type: 'Subgroup',
embedding: JSON.parse(embeddingExpression.data),
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
// has_channel_name is a typed XSD literal — no decode needed.
channelName: binding.channelName ?? '',
};
}),
);
Expand Down Expand Up @@ -153,7 +154,8 @@ export default class SemanticRelationship extends Ad4mModel {
type: typeNameMap[binding.type] || binding.type,
embedding: JSON.parse(embeddingExpression.data),
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
// Typed XSD literal.
channelName: binding.channelName ?? '',
};
}),
);
Expand Down Expand Up @@ -197,7 +199,8 @@ export default class SemanticRelationship extends Ad4mModel {
type: itemType,
embedding: JSON.parse(embeddingExpression.data),
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
// Typed XSD literal.
channelName: binding.channelName ?? '',
};
}),
);
Expand Down
11 changes: 6 additions & 5 deletions packages/api/src/topic/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Model, Ad4mModel, Flag, Property } from '@coasys/ad4m';
import { parseLit } from '../utils/parseLit';
import { SynergyMatch } from '@coasys/flux-utils';

export class TopicWithRelevance {
Expand Down Expand Up @@ -42,9 +41,10 @@ export default class Topic extends Ad4mModel {
dedupMap.set(id, {
id,
type: 'Conversation',
relevance: parseInt(parseLit(binding.relevance), 10) || 0,
// Typed XSD literals — SPARQL bindings give lexical form directly.
relevance: parseInt(binding.relevance ?? '', 10) || 0,
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
channelName: binding.channelName ?? '',
});
}
}
Expand Down Expand Up @@ -81,9 +81,10 @@ export default class Topic extends Ad4mModel {
dedupMap.set(id, {
id,
type: 'ConversationSubgroup',
relevance: parseInt(parseLit(binding.relevance), 10) || 0,
// Typed XSD literals — no decode needed.
relevance: parseInt(binding.relevance ?? '', 10) || 0,
channelId: binding.channelId,
channelName: parseLit(binding.channelName),
channelName: binding.channelName ?? '',
});
}
}
Expand Down
10 changes: 8 additions & 2 deletions packages/api/src/utils/parseLit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@
expect(parseLit(url)).toBe('hello world');
});

it('extracts .data from JSON literal objects', () => {
// Note: prior to the typed-RDF-literals refactor, Flux stored message
// bodies as `Literal.from({ data: text }).toUrl()` envelopes and this
// helper extracted `.data` for legacy read compatibility. With scalar
// properties now stored as deterministic typed literals, envelope-wrapped
// objects arriving through SPARQL are treated uniformly — the whole
// object is JSON-stringified (matches @coasys/ad4m's `parseLit`).
it('JSON-stringifies JSON literal objects (including {data: ...} envelopes)', () => {
const { Literal } = require('@coasys/ad4m');
const url = Literal.from({ data: 'extracted' }).toUrl();
expect(parseLit(url)).toBe('extracted');
expect(parseLit(url)).toBe(JSON.stringify({ data: 'extracted' }));

Check failure on line 39 in packages/api/src/utils/parseLit.test.ts

View workflow job for this annotation

GitHub Actions / tests

src/utils/parseLit.test.ts > parseLit > JSON-stringifies JSON literal objects (including {data: ...} envelopes)

AssertionError: expected 'extracted' to be '{"data":"extracted"}' // Object.is equality Expected: "{"data":"extracted"}" Received: "extracted" ❯ src/utils/parseLit.test.ts:39:27

Check failure on line 39 in packages/api/src/utils/parseLit.test.ts

View workflow job for this annotation

GitHub Actions / build

src/utils/parseLit.test.ts > parseLit > JSON-stringifies JSON literal objects (including {data: ...} envelopes)

AssertionError: expected 'extracted' to be '{"data":"extracted"}' // Object.is equality Expected: "{"data":"extracted"}" Received: "extracted" ❯ src/utils/parseLit.test.ts:39:27

Check failure on line 39 in packages/api/src/utils/parseLit.test.ts

View workflow job for this annotation

GitHub Actions / tests

src/utils/parseLit.test.ts > parseLit > JSON-stringifies JSON literal objects (including {data: ...} envelopes)

AssertionError: expected 'extracted' to be '{"data":"extracted"}' // Object.is equality Expected: "{"data":"extracted"}" Received: "extracted" ❯ src/utils/parseLit.test.ts:39:27
});

it('JSON-stringifies objects without .data field', () => {
Expand Down
Loading