Perspectives

Perspectives: Agent-Centric Knowledge Graphs

Understanding Perspectives

Perspectives are semantic knowledge graphs inspired by the Semantic Web and Linked Data principles, but reimagined through an agent-centric lens. While traditional RDF graphs and Solid PODs are designed around data ownership, Perspectives emphasize that all knowledge is inherently subjective and tied to the agent who claims it.

Objective vs. Subjective Data

In AD4M, we distinguish between two types of data:

  1. Objective Data (Expressions):

    • Stored in Languages
    • Same content for any agent requesting a given URL
    • Cryptographically signed by their author
    • Example: A post stored at QmSocial123://post789
  2. Subjective Data (Perspectives):

    • Personal associations between Expressions
    • Owned by a specific agent
    • Represent that agent's view of relationships
    • Example: Agent Alice linking a post as "important" or "related-to" another post

Links: Agent-Centric Triples

Perspectives consist of Links – our agent-centric version of RDF triples. Each Link connects three URIs:

interface Link {
    source: string;      // Subject Expression URL
    predicate: string;   // Predicate Expression URL
    target: string;      // Object Expression URL
}

These Links are stored as LinkExpressions – special Expressions where the data field contains a Link:

{
    author: "did:key:z6Mk...",  // Who made this association
    timestamp: "2023-06-21...",
    data: {
        source: "QmSocial123://post789",
        predicate: "sioc://likes",
        target: "did:key:z6Mk..."
    },
    proof: { ... }  // Cryptographic proof of the claim
}

Subjective Overlays

Perspectives act as subjective overlays on the objective Expression layer:

  • Every node in the graph must be a URI pointing to an Expression
  • The same Expressions can appear in many Perspectives with different relationships
  • Each agent can maintain multiple Perspectives for different contexts
  • Links in a Perspective represent an agent's claims about relationships

For example:

Agent Alice's Perspective:
Post1 --likes--> Post2
Post2 --related-to--> Post3

Agent Bob's Perspective:
Post1 --disagrees-with--> Post2
Post2 --authored-by--> Carol

Storage and Sharing

Perspectives are stored locally in your AD4M instance's database, but can be shared in several ways:

  1. Snapshots:

    • Export a Perspective as an Expression
    • Contains a JSON rendering of all Links
    • Useful for point-in-time sharing
  2. Neighbourhoods:

    • Collaborative spaces where multiple agents share and sync Perspectives
    • Real-time updates and p2p synchronization
    • See Neighbourhoods for more details

Future Standards Integration

While AD4M currently uses its own formats to accommodate agent-centric features, we plan to implement W3C, RDF, and Solid compatibility:

  • Every Perspective will be accessible as a Solid POD
  • Links will be expressible as RDF triples
  • Standard SPARQL queries will be supported

Working with Perspectives

The PerspectiveProxy class provides a rich interface for working with Perspectives:

// Create a new Perspective
const perspective = await ad4m.perspective.add("My Knowledge Graph");
 
// Add a Link
await perspective.add({
    source: "QmSocial123://post789",
    predicate: "sioc://likes",
    target: "did:key:z6Mk..."
});
 
// Query Links
const links = await perspective.get({
    source: "QmSocial123://post789"
});

SPARQL Query Engine

Every Perspective includes a high-performance SPARQL query engine powered by Oxigraph, executing entirely server-side in Rust. SPARQL is the recommended query method for most use cases:

// Direct SPARQL query - fast and powerful
const follows = await perspective.querySparql(`
  SELECT ?source ?target WHERE {
    ?link a rdf:Statement ;
          rdf:subject ?source ;
          rdf:predicate <follows> ;
          rdf:object ?target .
  }
`);
 
// Find who Alice follows
const aliceFollows = await perspective.querySparql(`
  SELECT ?target WHERE {
    ?link a rdf:Statement ;
          rdf:subject <user://alice> ;
          rdf:predicate <follows> ;
          rdf:object ?target .
  }
`);
 
// Find who follows Alice (reverse traversal)
const aliceFollowers = await perspective.querySparql(`
  SELECT ?source WHERE {
    ?link a rdf:Statement ;
          rdf:subject ?source ;
          rdf:predicate <follows> ;
          rdf:object <user://alice> .
  }
`);
 
// Aggregations and analytics
const stats = await perspective.querySparql(`
  SELECT ?predicate (COUNT(?link) AS ?total) WHERE {
    ?link a rdf:Statement ;
          rdf:predicate ?predicate .
  }
  GROUP BY ?predicate
`);

Security Note: querySparql() only allows read-only SELECT queries. Use perspective.add() and perspective.remove() to modify links.

For comprehensive examples and graph traversal patterns, see the SPARQL Queries Guide.

Prolog Integration (Legacy)

Perspectives also include a Prolog engine for backward compatibility and advanced logic programming:

// Basic Prolog query
const results = await perspective.queryProlog(
  'triple(Post, "sioc://likes", Agent), triple(Agent, "foaf://knows", "did:key:z6Mk...")'
);
// Finds posts liked by agents who know a specific person
 
// Real-time query subscription
const subscription = await perspective.subscribeInfer(
  'triple(Post, "sioc://status", "active"), triple(Post, "sioc://author", Author)'
);
 
// Set up callback for updates
subscription.onResult(results => {
    console.log("Active posts and their authors:", results);
});
 
// Important: Clean up subscription when done
subscription.dispose();

Query subscriptions provide real-time updates but require proper cleanup:

  1. Initialization: When you create a subscription, it waits for the first result before becoming active
  2. Keepalive: The subscription stays active through periodic keepalive signals
  3. Updates: Receive real-time updates through your callback as the data changes
  4. Cleanup: Call dispose() when done to:
    • Stop keepalive signals
    • Unsubscribe from updates
    • Release this subscriber's hold on the backend subscription
    • Prevent memory leaks

The backend keeps one subscription per distinct query and agent and shares it between all subscribers of that query. Disposing one of them does not interrupt the others; the backend removes the subscription when the last subscriber disposes or when nobody has sent a keepalive for a while. Calling dispose() again on the same subscription does nothing, so it cannot release another subscriber's hold.

Always remember to call dispose() when you're done with a subscription.

Recommendation: Use SPARQL for most queries, and reserve Prolog for complex logic programming or custom SDNA rules.

Advanced Data Modeling

AD4M provides a sophisticated data modeling system built on SHACL (Shapes Constraint Language) schemas. This allows you to:

  • Define TypeScript classes that map to semantic structures
  • Use decorators to specify relationships
  • Query with type safety and IDE support

For details on this powerful feature, see our guide on Model Classes.

Basic Usage Examples

Creating a Perspective:

const myNotes = ad4m.perspective.add("My private notes");

The returning object will be an instance of PerspectiveProxy – which essentially will work as your database instance.

Adding a Link:

const link = {
  source: "did:key:zQ3shv5VUAbtY5P1jGsPzy7L27FTxeDv2hnhVp9QT3ALNCnQ2",
  predicate: "sioc://likes",
  target: "literal:ad4m",
};
 
myNotes.add(link);

Query Links:

const allLinks = await myNotes.get(
  new LinkQuery({ predicate: "sioc://likes" })
);

What you get back will be an array of LinkExpressions:

[
  {
    "author": "did:key:zQ3shv5VUAbtY5P1jGsPzy7L27FTxeDv2hnhVp9QT3ALNCnQ2",
    "timestamp": "2023-06-21T14:47:48.935Z",
    "data": {
      "source": "did:key:zQ3shv5VUAbtY5P1jGsPzy7L27FTxeDv2hnhVp9QT3ALNCnQ2",
      "predicate": "sioc://likes",
      "target": "literal:ad4m"
    },
    "proof": {
      "key": "#zQ3shNWd4bg67ktTVg9EMnnrsRjhkH6cRNCjRRxfTaTqBniAf",
      "signature": "xxxx"
    }
  }
]

Each link comes back with the exact target it was signed over. The store indexes literal:string:, literal:number:, literal:boolean: and literal:json: targets as typed values, so a query finds a value however it was percent-encoded, but get() still returns the target as written, including literals not built with Literal.from(value).toUrl() (for example literal:string:Hello world with a raw space). That keeps the proof valid when a read-back link is passed on, for example when the Perspective is later published as a Neighbourhood.

Even though this Perspective is not shared (yet) but just our private, local graph database, we might want to share it as Neighbourhood.