Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
0e136d1
Initial experiments adding query logic to SubjectEntity all function
jhweir Feb 26, 2025
e4e73a0
Cache subject class name in SubjectEntity
lucksus Feb 27, 2025
81f5322
Unused id prop removed from getData
jhweir Feb 27, 2025
9ee94df
propertyGettersQuery function set up, used in getData() & findAll(), …
jhweir Feb 27, 2025
b589f07
Resolve subject properties in client if needed
lucksus Feb 28, 2025
5609e6b
Resolve & class name fixes, prop name updates for clarity
jhweir Mar 2, 2025
50b2855
mapPropertiesToInstance function set up
jhweir Mar 2, 2025
1db991c
fmt
jhweir Mar 2, 2025
7358493
Timestamp & author included in getData query
jhweir Mar 2, 2025
9242ea7
collectionGetterQuery set up
jhweir Mar 2, 2025
954521c
SubjectClass defintion extracted from getter queries
jhweir Mar 2, 2025
18539a5
Include collections in findAll
jhweir Mar 2, 2025
f46f572
Include timestamp & author in findAll
jhweir Mar 2, 2025
5c6d309
Prolog findall predicate moved to getter functions
jhweir Mar 2, 2025
b773ffd
authorAndTimeStampQuery extracted
jhweir Mar 3, 2025
1b578db
Common prolog queries stores as consts
jhweir Mar 3, 2025
8532812
Property name changes
jhweir Mar 3, 2025
e0d0787
Updated prolog-and-literals tests
jhweir Mar 3, 2025
d26ffce
Properties query set up with tests
jhweir Mar 3, 2025
40b6211
Collections query set up with tests
jhweir Mar 3, 2025
6338b54
buildAuthorAndTimestampQuery function set up, code refactor
jhweir Mar 3, 2025
19c3d9e
Formatting, notes, & Query type
jhweir Mar 3, 2025
5501ce9
Initial where query set up
jhweir Mar 4, 2025
53345d9
Test findAll() works with a mix of query constraints
jhweir Mar 4, 2025
51d97dd
Where query works with timestamp & author
jhweir Mar 4, 2025
4208462
Where query works with not operations
jhweir Mar 4, 2025
4187490
Where query works with arrays of timestamps and authors
jhweir Mar 4, 2025
5a58e76
Convert number strings to numbers in literal_from_url predicate
jhweir Mar 5, 2025
f417090
Boolean URI set up & boolean conversion in literal_from_url predicate
jhweir Mar 5, 2025
2c4c4a7
New where query structure & operation testing
jhweir Mar 6, 2025
6e88948
Where query reorganised, comments added
jhweir Mar 6, 2025
12fe76d
Old methods removed
jhweir Mar 6, 2025
ca67fb5
Source prop added to query params
jhweir Mar 6, 2025
296c7df
WhereOps type, ops formatting
jhweir Mar 7, 2025
8b47679
Where prop default value fix
jhweir Mar 7, 2025
dc26e99
In Prolog property resolution for literals
lucksus Mar 7, 2025
9fd9994
“where” constraint on content of resolved property
lucksus Mar 7, 2025
de1f445
sort_instances predciate set up
jhweir Mar 8, 2025
05d00fa
Ordering set up
jhweir Mar 8, 2025
2c7bbfb
Ordering tests
jhweir Mar 8, 2025
26abe17
Limit & offset set up with tests
jhweir Mar 8, 2025
d75041b
Less than or equal to & greater than or equal to set up with tests
jhweir Mar 9, 2025
883a0e3
formatValue updates
jhweir Mar 9, 2025
64cd4fd
lt, lte, gt, & gte operation handling reorganised
jhweir Mar 9, 2025
9da1842
Broken tests & logic commented out for now
jhweir Mar 9, 2025
f731547
sdna.rs fmt
jhweir Mar 9, 2025
6e26d7e
Manual literal creation in tests removed
jhweir Mar 10, 2025
74e55a8
Make json_property/3 work with number and boolean values
lucksus Mar 10, 2025
f5c25c3
Use PROPERTY_RESOLVE_QUERY again
lucksus Mar 10, 2025
b4f00e6
Replace dangerous cut-operator if plain logic
lucksus Mar 10, 2025
cbeb3b1
Leave “name” prop to not resolve
lucksus Mar 10, 2025
97c7601
Make “name” resolved again, add unresolved “plain” and adjust assertions
lucksus Mar 10, 2025
341064e
Unflake test that uses timestamp by adding sleeps
lucksus Mar 10, 2025
c0d9bed
Extract parts from findAll() into query building and result parsing f…
lucksus Mar 11, 2025
a6b9c5e
QueryBuilder with subscribeAndRun(callback)
lucksus Mar 11, 2025
5b6c5af
Test and fix query subscriptions actually returning correct data
lucksus Mar 11, 2025
74e43a4
JSON parse subscription query results, AllInstancesResult type added
jhweir Mar 17, 2025
ebabdb6
Try catch block added to subcription query JSON parsing
jhweir Mar 17, 2025
3c525c0
JSON parsing removed from integration tests
jhweir Mar 17, 2025
cb5c0c7
Update core/src/subject/SubjectEntity.ts
lucksus Mar 17, 2025
0d30380
Author & Timestamp always included in queries (no longer optional)
jhweir Mar 18, 2025
6865a11
Unused types removed
jhweir Mar 18, 2025
1bccf1f
Remove unused and commented-out test case
lucksus Mar 18, 2025
5929dd7
Option to pass single Query object to the query fucntion set up
jhweir Mar 18, 2025
b3e13df
Test to ensure SubjectQueryBuilder uses the right class to match to
lucksus Mar 18, 2025
18a41ac
Fix assingValuesToInstance in case resolving URI failed
lucksus Mar 18, 2025
ab52e2a
Fix static classNameByClass cache to actually work for sub classes
lucksus Mar 18, 2025
f52a815
Fix test assertion of new SubjectQueryBuilder test
lucksus Mar 18, 2025
8207a1f
Formatting & unused type export removed
jhweir Mar 18, 2025
4ec39f3
Fix SubjectQueryBuilder to use constructor of subclass for correct ma…
lucksus Mar 18, 2025
31eb925
Merge branch 'ad4m-record' of github.com:coasys/ad4m into ad4m-record
lucksus Mar 18, 2025
0997b27
Increase timeout on timestamp based test to avoid flaky results
lucksus Mar 18, 2025
313819a
Turn PROPERTY_RESOLVE_QUERY into built-in predicate
lucksus Mar 18, 2025
6b4e20e
Unflake, complex query builder test for single query object
lucksus Mar 18, 2025
b2be6a9
Test: findAll() works with multiple property constraints in one where…
lucksus Mar 18, 2025
5022686
Use inline class instead of Recipe in “query builder works with subsc…
lucksus Mar 18, 2025
68fe13c
Use inline class instead of Recipe in “findAll() works with where que…
lucksus Mar 18, 2025
24bf555
Merge branch 'dev' into ad4m-record
lucksus Mar 18, 2025
be395af
Changelog
lucksus Mar 18, 2025
7af0551
Error handling in instancesFromPrologResult, export type Query, & for…
jhweir Mar 19, 2025
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
6 changes: 4 additions & 2 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project _loosely_ adheres to [Semantic Versioning](https://semver.org/spec/
- Fix error after spawning AI task [PR#559](https://github.com/coasys/ad4m/pull/559)
- Fix some problems with perspective.removeLinks() with a proper implementation [PR#563](https://github.com/coasys/ad4m/pull/563)
- Ad4mConnect detects running local ADAM agent on first open - no need to click try again [](https://github.com/coasys/ad4m/pull/570)
- Fix multiple synchronous addSignalHandler calls losing the first handler - fixing usage of NH.sendSignal() [PR#576](https://github.com/coasys/ad4m/pull/576)

### Added
- Prolog predicates needed in new Flux mention notification trigger:
Expand Down Expand Up @@ -52,8 +53,9 @@ This project _loosely_ adheres to [Semantic Versioning](https://semver.org/spec/
- Models can also be added straight from a local file [PR#565](https://github.com/coasys/ad4m/pull/565)
- Enable fine-tuning of voice detection parameters for transcription streams [PR#566](https://github.com/coasys/ad4m/pull/566)
- Neighbourhood p2p signal: optional loopback sending signals to all connected apps/UIs of the sending agent [PR#568](https://github.com/coasys/ad4m/pull/568)
- DB and Perspective to JSON exports and imports [PR#569](https://github.com/coasys/ad4m/pull/569)

- DB and Perspective to JSON exports and imports [PR#569](https://github.com/coasys/ad4m/pull/569)
- Enhanced SubjectEntity query mechanism with where-queries, that compiles complex queries down to Prolog which can be subscribed to [PR#575](https://github.com/coasys/ad4m/pull/575)

### Changed
- Partially migrated the Runtime service to Rust. (DM language installation for agents is pending.) [PR#466](https://github.com/coasys/ad4m/pull/466)
- Improved performance of SDNA / SubjectClass functions by moving code from client into executor and saving a lot of client <-> executor roundtrips [PR#480](https://github.com/coasys/ad4m/pull/480)
Expand Down
5 changes: 4 additions & 1 deletion core/src/Literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class Literal {
toUrl(): string {
if(this.#url && !this.#literal)
return this.#url
if(!this.#url && !this.#literal)
if(!this.#url && (this.#literal === undefined || this.#literal === "" || this.#literal === null))
throw new Error("Can't turn empty Literal into URL")

let encoded
Expand All @@ -38,6 +38,9 @@ export class Literal {
case 'number':
encoded = `number:${encodeRFC3986URIComponent(this.#literal)}`
break;
case 'boolean':
encoded = `boolean:${encodeRFC3986URIComponent(this.#literal)}`
break;
case 'object':
encoded = `json:${encodeRFC3986URIComponent(JSON.stringify(this.#literal))}`
break;
Expand Down
23 changes: 18 additions & 5 deletions core/src/perspectives/PerspectiveClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Perspective } from "./Perspective";
import { PerspectiveHandle, PerspectiveState } from "./PerspectiveHandle";
import { LinkStatus, PerspectiveProxy } from './PerspectiveProxy';
import { AIClient } from "../ai/AIClient";
import { AllInstancesResult } from "../subject/SubjectEntity";

const LINK_EXPRESSION_FIELDS = `
author
Expand Down Expand Up @@ -154,7 +155,7 @@ export class PerspectiveClient {
return JSON.parse(perspectiveQueryProlog)
}

async subscribeQuery(uuid: string, query: string): Promise<{ subscriptionId: string, result: string }> {
async subscribeQuery(uuid: string, query: string): Promise<{ subscriptionId: string, result: AllInstancesResult }> {
const { perspectiveSubscribeQuery } = unwrapApolloResult(await this.#apolloClient.mutate({
mutation: gql`mutation perspectiveSubscribeQuery($uuid: String!, $query: String!) {
perspectiveSubscribeQuery(uuid: $uuid, query: $query) {
Expand All @@ -164,11 +165,17 @@ export class PerspectiveClient {
}`,
variables: { uuid, query }
}))

return perspectiveSubscribeQuery
const { subscriptionId, result } = perspectiveSubscribeQuery
let finalResult = result;
try {
finalResult = JSON.parse(result)
} catch (e) {
console.error('Error parsing perspectiveSubscribeQuery result:', e)
}
return { subscriptionId, result: finalResult }
}

subscribeToQueryUpdates(subscriptionId: string, onData: (result: string) => void): () => void {
subscribeToQueryUpdates(subscriptionId: string, onData: (result: AllInstancesResult) => void): () => void {
const subscription = this.#apolloClient.subscribe({
query: gql`
subscription perspectiveQuerySubscription($subscriptionId: String!) {
Expand All @@ -181,7 +188,13 @@ export class PerspectiveClient {
}).subscribe({
next: (result) => {
if (result.data && result.data.perspectiveQuerySubscription) {
onData(result.data.perspectiveQuerySubscription);
let finalResult = result.data.perspectiveQuerySubscription;
try {
finalResult = JSON.parse(result.data.perspectiveQuerySubscription)
} catch (e) {
console.error('Error parsing perspectiveQuerySubscription:', e)
}
onData(finalResult);
}
},
error: (e) => console.error('Error in query subscription:', e)
Expand Down
19 changes: 10 additions & 9 deletions core/src/perspectives/PerspectiveProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { NeighbourhoodExpression } from "../neighbourhood/Neighbourhood";
import { AIClient } from "../ai/AIClient";
import { PERSPECTIVE_QUERY_SUBSCRIPTION } from "./PerspectiveResolver";
import { gql } from "@apollo/client/core";
import { AllInstancesResult } from "../subject/SubjectEntity";

type QueryCallback = (result: string) => void;
type QueryCallback = (result: AllInstancesResult) => void;

// Generic subscription interface that matches Apollo's Subscription
interface Unsubscribable {
Expand Down Expand Up @@ -53,7 +54,7 @@ export class QuerySubscriptionProxy {
#callbacks: Set<QueryCallback>;
#keepaliveTimer: number;
#unsubscribe?: () => void;
#latestResult: string;
#latestResult: AllInstancesResult;
#disposed: boolean = false;

/** Creates a new query subscription
Expand All @@ -62,7 +63,7 @@ export class QuerySubscriptionProxy {
* @param initialResult - The initial query result
* @param client - The PerspectiveClient instance to use for communication
*/
constructor(uuid: string, subscriptionId: string, initialResult: string, client: PerspectiveClient) {
constructor(uuid: string, subscriptionId: string, initialResult: AllInstancesResult, client: PerspectiveClient) {
this.#uuid = uuid;
this.#subscriptionId = subscriptionId;
this.#client = client;
Expand Down Expand Up @@ -109,7 +110,7 @@ export class QuerySubscriptionProxy {
*
* @returns The latest query result as a string (usually a JSON array of bindings)
*/
get result(): string {
get result(): AllInstancesResult {
return this.#latestResult;
}

Expand Down Expand Up @@ -138,7 +139,7 @@ export class QuerySubscriptionProxy {
}

/** Internal method to notify all callbacks of a new result */
#notifyCallbacks(result: string) {
#notifyCallbacks(result: AllInstancesResult) {
for (const callback of this.#callbacks) {
try {
callback(result);
Expand Down Expand Up @@ -450,7 +451,7 @@ export class PerspectiveProxy {
}
}

async stringOrTemplateObjectToSubjectClass<T>(subjectClass: T): Promise<string> {
async stringOrTemplateObjectToSubjectClassName<T>(subjectClass: T): Promise<string> {
if(typeof subjectClass === "string")
return subjectClass
else {
Expand Down Expand Up @@ -505,7 +506,7 @@ export class PerspectiveProxy {
* @param exprAddr The address of the expression to be turned into a subject instance
*/
async removeSubject<T>(subjectClass: T, exprAddr: string) {
let className = await this.stringOrTemplateObjectToSubjectClass(subjectClass)
let className = await this.stringOrTemplateObjectToSubjectClassName(subjectClass)
let result = await this.infer(`subject_class("${className}", C), destructor(C, Actions)`)
if(!result.length) {
throw "No constructor found for given subject class: " + className
Expand All @@ -522,7 +523,7 @@ export class PerspectiveProxy {
* that matches the given properties will be used.
*/
async isSubjectInstance<T>(expression: string, subjectClass: T): Promise<boolean> {
let className = await this.stringOrTemplateObjectToSubjectClass(subjectClass)
let className = await this.stringOrTemplateObjectToSubjectClassName(subjectClass)
let isInstance = false;
const maxAttempts = 5;
let attempts = 0;
Expand All @@ -549,7 +550,7 @@ export class PerspectiveProxy {
if(!await this.isSubjectInstance(base, subjectClass)) {
throw `Expression ${base} is not a subject instance of given class: ${JSON.stringify(subjectClass)}`
}
let className = await this.stringOrTemplateObjectToSubjectClass(subjectClass)
let className = await this.stringOrTemplateObjectToSubjectClassName(subjectClass)
let subject = new Subject(this, base, className)
await subject.init()
return subject as unknown as T
Expand Down
32 changes: 16 additions & 16 deletions core/src/subject/Subject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import { collectionSetterToName, collectionToAdderName, collectionToRemoverName,
*/
export class Subject {
#baseExpression: string;
#subjectClass: string;
#subjectClassName: string;
#perspective: PerspectiveProxy

/**
* Constructs a new subject.
* @param perspective - The perspective that the subject belongs to.
* @param baseExpression - The base expression of the subject.
* @param subjectClass - The class of the subject.
* @param subjectClassName - The class name of the subject.
*/
constructor(perspective: PerspectiveProxy, baseExpression: string, subjectClass: string) {
constructor(perspective: PerspectiveProxy, baseExpression: string, subjectClassName: string) {
this.#baseExpression = baseExpression
this.#subjectClass = subjectClass
this.#subjectClassName = subjectClassName
this.#perspective = perspective
}

Expand All @@ -36,21 +36,21 @@ export class Subject {
*/
async init() {
// Check if the subject is a valid instance of the subject class
let isInstance = await this.#perspective.isSubjectInstance(this.#baseExpression, this.#subjectClass)
let isInstance = await this.#perspective.isSubjectInstance(this.#baseExpression, this.#subjectClassName)
if(!isInstance) {
throw `Not a valid subject instance of ${this.#subjectClass} for ${this.#baseExpression}`
throw `Not a valid subject instance of ${this.#subjectClassName} for ${this.#baseExpression}`
}

// Define properties and collections dynamically
let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property(C, Property)`)
let results = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), property(C, Property)`)
let properties = results.map(result => result.Property)

for(let p of properties) {
const resolveExpressionURI = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_resolve(C, "${p}")`)
const resolveExpressionURI = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), property_resolve(C, "${p}")`)
Object.defineProperty(this, p, {
configurable: true,
get: async () => {
let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_getter(C, "${this.#baseExpression}", "${p}", Value)`)
let results = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), property_getter(C, "${this.#baseExpression}", "${p}", Value)`)
if(results && results.length > 0) {
let expressionURI = results[0].Value
if(resolveExpressionURI) {
Expand Down Expand Up @@ -81,13 +81,13 @@ export class Subject {
}

// Define setters
const setters = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_setter(C, Property, Setter)`)
const setters = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), property_setter(C, Property, Setter)`)

for(let setter of (setters ? setters : [])) {
if(setter) {
const property = setter.Property
const actions = eval(setter.Setter)
const resolveLanguageResults = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_resolve_language(C, "${property}", Language)`)
const resolveLanguageResults = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), property_resolve_language(C, "${property}", Language)`)
let resolveLanguage
if(resolveLanguageResults && resolveLanguageResults.length > 0) {
resolveLanguage = resolveLanguageResults[0].Language
Expand All @@ -102,15 +102,15 @@ export class Subject {
}

// Define collections
let results2 = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection(C, Collection)`)
let results2 = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), collection(C, Collection)`)
if(!results2) results2 = []
let collections = results2.map(result => result.Collection)

for(let c of collections) {
Object.defineProperty(this, c, {
configurable: true,
get: async () => {
let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection_getter(C, "${this.#baseExpression}", "${c}", Value)`)
let results = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), collection_getter(C, "${this.#baseExpression}", "${c}", Value)`)
if(results && results.length > 0 && results[0].Value) {
let collectionContent = results[0].Value.filter((v: any) => v !== "" && v !== '')
return collectionContent
Expand All @@ -122,7 +122,7 @@ export class Subject {
}

// Define collection adders
let adders = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection_adder(C, Collection, Adder)`)
let adders = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), collection_adder(C, Collection, Adder)`)
if(!adders) adders = []

for(let adder of adders) {
Expand All @@ -140,7 +140,7 @@ export class Subject {
}

// Define collection removers
let removers = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection_remover(C, Collection, Remover)`)
let removers = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), collection_remover(C, Collection, Remover)`)
if(!removers) removers = []

for(let remover of removers) {
Expand All @@ -158,7 +158,7 @@ export class Subject {
}

// Define collection setters
let collectionSetters = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection_setter(C, Collection, Setter)`)
let collectionSetters = await this.#perspective.infer(`subject_class("${this.#subjectClassName}", C), collection_setter(C, Collection, Setter)`)
if(!collectionSetters) collectionSetters = []

for(let collectionSetter of collectionSetters) {
Expand Down
Loading