personal-knowledge-library


Namepersonal-knowledge-library JSON
Version 2.4.3 PyPI version JSON
download
home_pagehttps://github.com/Wacom-Developer/personal-knowledge-library
SummaryLibrary to access Wacom's Personal Knowledge graph.
upload_time2024-12-17 07:40:09
maintainerNone
docs_urlNone
authorMarkus Weber
requires_pythonNone
licenseApache 2.0 License
keywords semantic-knowledge;knowledge-graph
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Wacom Private Knowledge Library

[![Python package](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/python-package.yml/badge.svg)](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/python-package.yml)
[![Pylint](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/pylint.yml/badge.svg)](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/pylint.yml)

![License: Apache 2](https://img.shields.io/badge/License-Apache2-green.svg)
[![PyPI](https://img.shields.io/pypi/v/personal-knowledge-library.svg)](https://pypi.python.org/pypi/personal-knowledge-library)
[![PyPI](https://img.shields.io/pypi/pyversions/personal-knowledge-library.svg)](https://pypi.python.org/pypi/personal-knowledge-library)
[![Documentation](https://img.shields.io/badge/api-reference-blue.svg)](https://developer-docs.wacom.com/docs/private-knowledge-service) 

![Contributors](https://img.shields.io/github/contributors/Wacom-Developer/personal-knowledge-library.svg)
![GitHub forks](https://img.shields.io/github/forks/Wacom-Developer/personal-knowledge-library.svg)
![GitHub stars](https://img.shields.io/github/stars/Wacom-Developer/personal-knowledge-library.svg)

The required tenant API key is only available for selected partner companies.
Please contact your Wacom representative for more information.

## Introduction

In knowledge management there is a distinction between data, information and knowledge.
In the domain of digital ink this means:

- **Data** - The equivalent would be the ink strokes
- **Information** - After using handwriting-, shape-, math-, or other recognition processes ink strokes are converted into machine readable content, such as text, shapes, math representations, other other digital content
- **Knowledge / Semantics** -  Beyond recognition content needs to be semantically analysed to become semantically understood based on a shared common knowledge.

The following illustration shows the different layers of knowledge:
![Levels of ink knowledge layers](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/knowledge-levels.png)

For handling semantics, Wacom introduced the Wacom Private Knowledge (WPK) cloud service to manage personal ontologies and its associated personal knowledge graph.

This library provide simplified access to Wacom's personal knowledge cloud service.
It contains:

- Basic datastructures for Ontology object and entities from the knowledge graph
- Clients for the REST APIs
- Connector for Wikidata public knowledge graph

**Ontology service:**

- List all Ontology structures
- Modify Ontology structures
- Delete Ontology structures

**Entity service:**

- List all entities
- Add entities to knowledge graph
- Access object properties

**Search service:**

- Search for entities for labels and descriptions with a given language
- Search for literals (data properties) 
- Search for relations (object properties)

**Group service:**

- List all groups
- Add groups, modify groups, delete groups
- Add users and entities to groups

**Ontology service:**

- List all Ontology structures
- Modify Ontology structures

**Named Entity Linking service:**

- Linking words to knowledge entities from graph in a given text (Ontology-based Named Entity Linking)

**Wikidata connector:**

- Import entities from Wikidata
- Mapping Wikidata entities to WPK entities

# Technology stack

## Domain Knowledge

The tasks of the ontology within Wacom's private knowledge system is to formalised the domain the technology is used in, such as education-, smart home-, or creative domein.
The domain model will be the foundation for the entities collected within the knowledge graph, describing real world concepts in a formal language understood by artificial intelligence system:

- Foundation for structured data, knowledge representation as concepts and relations among concepts
- Being explicit definitions of shared vocabularies for interoperability
- Being actionable fragments of explicit knowledge that engines can use for inferencing (Reasoning)
- Can be used for problem solving

An ontology defines (specifies) the concepts, relationships, and other distinctions that are relevant for modelling a domain.

## Knowledge Graph

- Knowledge graph is generated from unstructured and structured knowledge sources
- Contains all structured knowledge gathered from all sources
- Foundation for all semantic algorithms

## Semantic Technology

- Extract knowledge from various sources (Connectors)
- Linking words to knowledge entities from graph in a given text (Ontology-based Named Entity Linking)
- Enables a smart search functionality which understands the context and finds related documents (Semantic Search)


# Functionality

## Import Format

For importing entities into the knowledge graph, the tools/import_entities.py script can be used.

The ThingObject support a NDJSON based import format, where the individual JSON files can contain the following structure.

| Field name             | Subfield name | Data Structure | Description                                                                                    |
|------------------------|---------------|----------------|------------------------------------------------------------------------------------------------|
| source_reference_id    |               | str            | A unique identifier for the entity used in the source system                                  |
| source_system          |               | str            | The source system describes the original source of the entity, such as wikidata, youtube, ... |
| image                  |               | str            | A string representing the URL of the entity's icon.                                           |
| labels                 |               | array          | An array of label objects, where each object has the following fields:                       |
|                        | value         | str            | A string representing the label text in the specified locale.                                |
|                        | locale        | str            | A string combining the ISO-3166 country code and the ISO-639 language code (e.g., "en-US").  |
|                        | isMain        | bool           | A boolean flag indicating if this label is the main label for the entity (true) or an alias (false). |
| descriptions           |               | array          | An array of description objects, where each object has the following fields:                 |
|                        | description   | str            | A string representing the description text in the specified locale.                          |
|                        | locale        | str            | A string combining the ISO-3166 country code and the ISO-639 language code (e.g., "en-US").  |
| type                   |               | str            | A string representing the IRI of the ontology class for this entity.                         |
| literals               |               | array[map]     | An array of data property objects, where each object has the following fields:               |


## Access API

The personal knowledge graph backend is implement as a multi-tenancy system.
Thus, several tenants can be logically separated from each other and different organisations can build their one knowledge graph.

![Tenant concept](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/tenant-concept.png)

In general, a tenant with their users, groups, and entities are logically separated.
Physically the entities are store in the same instance of the Wacom Private Knowledge (WPK) backend database system.

The user management is rather limited, each organisation must provide their own authentication service and user management.
The backend only has a reference of the user (*“shadow user”*) by an **external user id**.

The management of tenants is limited to the system owner - Wacom -, as it requires a **tenant management API** key.
While users for each tenant can be created by the owner of the **Tenant API Key**.
You will receive this token from the system owner after the creation of the tenant.


> :warning: Store the **Tenant API Key** in a secure key store, as attackers can use the key to harm your system.


The **Tenant API Key** should be only used by your authentication service to create shadow users and to login your user into the WPK backend.
After a successful user login, you will receive a token which can be used by the user to create, update, or delete entities and relations.

The following illustration summarizes the flows for creation of tenant and users:

![Tenant and user creation](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/tenant-user-creation.png)

The organisation itself needs to implement their own authentication service which:

- handles the users and their passwords,
- controls the personal data of the users,
- connects the users with the WPK backend and share with them the user token.

The WPK backend only manages the access levels of the entities and the group management for users.
The illustration shows how the access token is received from the WPK endpoint:

![Access token request.](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/access-token.png)

# Entity API

The entities used within the knowledge graph and the relationship among them is defined within an ontology that is manage with Wacom Ontology Management System (WOMS).

An entity within the personal knowledge graphs consist of these major parts:

- **Icon** - a visual representation of the entity, for instance a portrait of a person.
- **URI** - a unique resource identifier of an entity in the graph.
- **Type** - the type links to the defined concept class in the ontology.
- **Labels** - labels are the word(s) use in a language for the concept.
- **Description** - a short abstract that describes the entity.
- **Literals** - literals are properties of an entity, such as first name of a person. The ontology defines all literals of the concept class as well as its data type.
- **Relations** - the relationship among different entities is described using relations.

The following illustration provides an example for an entity:

![Entity description](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/entity-description.png)

## Entity content

Entities in general are language-independent as across nationalities or cultures we only use different scripts and words for a shared instance of a concept.

Let's take Leonardo da Vinci as an example.
The ontology defines the concept of a Person, a human being.
Now, in English its label would be _Leonardo da Vinci_, while in Japanese _レオナルド・ダ・ヴィンチ_.
Moreover, he is also known as _Leonardo di ser Piero da Vinci_ or _ダ・ビンチ_.

### Labels

Now, in the given example all words that a assigned to the concept are labels.
The label _Leonardo da Vinci_ is stored in the backend with an additional language code, e.g. _en_.

There is always a main label, which refers to the most common or official name of entity.
Another example would be Wacom, where _Wacom Co., Ltd._ is the official name while _Wacom_ is commonly used and be considered as an alias.

>  :pushpin: For the language code the **ISO 639-1:2002**, codes for the representation of names of languages—Part 1: Alpha-2 code. Read more, [here](https://www.iso.org/standard/22109.html)

## Samples

### Entity handling

This samples shows how to work with graph service.

```python
import argparse
from typing import Optional, Dict, List

from knowledge.base.entity import Description, Label
from knowledge.base.language import LocaleCode, EN_US, DE_DE
from knowledge.base.ontology import OntologyClassReference, OntologyPropertyReference, ThingObject, ObjectProperty
from knowledge.services.graph import WacomKnowledgeService

# ------------------------------- Knowledge entities -------------------------------------------------------------------
LEONARDO_DA_VINCI: str = 'Leonardo da Vinci'
SELF_PORTRAIT_STYLE: str = 'self-portrait'
ICON: str = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/Mona_Lisa_%28copy%2C_Thalwil%2C_Switzerland%29."\
            "JPG/1024px-Mona_Lisa_%28copy%2C_Thalwil%2C_Switzerland%29.JPG"
# ------------------------------- Ontology class names -----------------------------------------------------------------
THING_OBJECT: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Thing')
"""
The Ontology will contain a Thing class where is the root class in the hierarchy. 
"""
ARTWORK_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'creative', 'VisualArtwork')
PERSON_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Person')
ART_STYLE_CLASS: OntologyClassReference = OntologyClassReference.parse('wacom:creative#ArtStyle')
IS_CREATOR: OntologyPropertyReference = OntologyPropertyReference('wacom', 'core', 'created')
HAS_TOPIC: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:core#hasTopic')
CREATED: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:core#created')
HAS_ART_STYLE: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:creative#hasArtstyle')


def print_entity(display_entity: ThingObject, list_idx: int, client: WacomKnowledgeService,
                 short: bool = False):
    """
    Printing entity details.

    Parameters
    ----------
    display_entity: ThingObject
        Entity with properties
    list_idx: int
        Index with a list
    client: WacomKnowledgeService
        Knowledge graph client
    short: bool
        Short summary
    """
    print(f'[{list_idx}] : {display_entity.uri} <{display_entity.concept_type.iri}>')
    if len(display_entity.label) > 0:
        print('    | [Labels]')
        for la in display_entity.label:
            print(f'    |     |- "{la.content}"@{la.language_code}')
        print('    |')
    if not short:
        if len(display_entity.alias) > 0:
            print('    | [Alias]')
            for la in display_entity.alias:
                print(f'    |     |- "{la.content}"@{la.language_code}')
            print('    |')
        if len(display_entity.data_properties) > 0:
            print('    | [Attributes]')
            for data_property, labels in display_entity.data_properties.items():
                print(f'    |    |- {data_property.iri}:')
                for li in labels:
                    print(f'    |    |-- "{li.value}"@{li.language_code}')
            print('    |')

        relations_obj: Dict[OntologyPropertyReference, ObjectProperty] = client.relations(uri=display_entity.uri)
        if len(relations_obj) > 0:
            print('    | [Relations]')
            for r_idx, re in enumerate(relations_obj.values()):
                last: bool = r_idx == len(relations_obj) - 1
                print(f'    |--- {re.relation.iri}: ')
                print(f'    {"|" if not last else " "}       |- [Incoming]: {re.incoming_relations} ')
                print(f'    {"|" if not last else " "}       |- [Outgoing]: {re.outgoing_relations}')
        print()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default='https://private-knowledge.wacom.com',
                        help="URL of instance")
    args = parser.parse_args()
    TENANT_KEY: str = args.tenant
    EXTERNAL_USER_ID: str = args.user
    # Wacom personal knowledge REST API Client
    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(application_name="Wacom Knowledge Listing",
                                                                    service_url=args.instance)
    knowledge_client.login(args.tenant, args.user)
    page_id: Optional[str] = None
    page_number: int = 1
    entity_count: int = 0
    print('-----------------------------------------------------------------------------------------------------------')
    print(' First step: Find Leonardo da Vinci in the knowledge graph.')
    print('-----------------------------------------------------------------------------------------------------------')
    res_entities, next_search_page = knowledge_client.search_labels(search_term=LEONARDO_DA_VINCI,
                                                                    language_code=LocaleCode('en_US'), limit=1000)
    leo: Optional[ThingObject] = None
    s_idx: int = 1
    for res_entity in res_entities:
        #  Entity must be a person and the label match with full string
        if res_entity.concept_type == PERSON_CLASS and LEONARDO_DA_VINCI in [la.content for la in res_entity.label]:
            leo = res_entity
            break

    print('-----------------------------------------------------------------------------------------------------------')
    print(' What artwork exists in the knowledge graph.')
    print('-----------------------------------------------------------------------------------------------------------')
    relations_dict: Dict[OntologyPropertyReference, ObjectProperty] = knowledge_client.relations(uri=leo.uri)
    print(f' Artwork of {leo.label}')
    print('-----------------------------------------------------------------------------------------------------------')
    idx: int = 1
    if CREATED in relations_dict:
        for e in relations_dict[CREATED].outgoing_relations:
            print(f' [{idx}] {e.uri}: {e.label}')
            idx += 1
    print('-----------------------------------------------------------------------------------------------------------')
    print(' Let us create a new piece of artwork.')
    print('-----------------------------------------------------------------------------------------------------------')

    # Main labels for entity
    artwork_labels: List[Label] = [
        Label('Ginevra Gherardini', EN_US),
        Label('Ginevra Gherardini', DE_DE)
    ]
    # Alias labels for entity
    artwork_alias: List[Label] = [
        Label("Ginevra", EN_US),
        Label("Ginevra", DE_DE)
    ]
    # Topic description
    artwork_description: List[Description] = [
        Description('Oil painting of Mona Lisa\' sister', EN_US),
        Description('Ölgemälde von Mona Lisa\' Schwester', DE_DE)
    ]
    # Topic
    artwork_object: ThingObject = ThingObject(label=artwork_labels, concept_type=ARTWORK_CLASS,
                                              description=artwork_description,
                                              icon=ICON)
    artwork_object.alias = artwork_alias
    print(f' Create: {artwork_object}')
    # Create artwork
    artwork_entity_uri: str = knowledge_client.create_entity(artwork_object)
    print(f' Entity URI: {artwork_entity_uri}')
    # Create relation between Leonardo da Vinci and artwork
    knowledge_client.create_relation(source=leo.uri, relation=IS_CREATOR, target=artwork_entity_uri)

    relations_dict = knowledge_client.relations(uri=artwork_entity_uri)
    for ontology_property, object_property in relations_dict.items():
        print(f'  {object_property}')
    # You will see that wacom:core#isCreatedBy is automatically inferred as relation as it is the inverse property of
    # wacom:core#created.

    # Now, more search options
    res_entities, next_search_page = knowledge_client.search_description('Michelangelo\'s Sistine Chapel',
                                                                         EN_US, limit=1000)
    print('-----------------------------------------------------------------------------------------------------------')
    print(' Search results.  Description: "Michelangelo\'s Sistine Chapel"')
    print('-----------------------------------------------------------------------------------------------------------')
    s_idx: int = 1
    for e in res_entities:
        print_entity(e, s_idx, knowledge_client)

    # Now, let's search all artwork that has the art style self-portrait
    res_entities, next_search_page = knowledge_client.search_labels(search_term=SELF_PORTRAIT_STYLE,
                                                                    language_code=EN_US, limit=1000)
    art_style: Optional[ThingObject] = None
    s_idx: int = 1
    for entity in res_entities:
        #  Entity must be a person and the label match with full string
        if entity.concept_type == ART_STYLE_CLASS and SELF_PORTRAIT_STYLE in [la.content for la in entity.label]:
            art_style = entity
            break
    res_entities, next_search_page = knowledge_client.search_relation(subject_uri=None,
                                                                      relation=HAS_ART_STYLE,
                                                                      object_uri=art_style.uri,
                                                                      language_code=EN_US)
    print('-----------------------------------------------------------------------------------------------------------')
    print(' Search results.  Relation: relation:=has_topic  object_uri:= unknown')
    print('-----------------------------------------------------------------------------------------------------------')
    s_idx: int = 1
    for e in res_entities:
        print_entity(e, s_idx, knowledge_client, short=True)
        s_idx += 1

    # Finally, the activation function retrieving the related identities to a pre-defined depth.
    entities, relations = knowledge_client.activations(uris=[leo.uri], depth=1)
    print('-----------------------------------------------------------------------------------------------------------')
    print(f'Activation.  URI: {leo.uri}')
    print('-----------------------------------------------------------------------------------------------------------')
    s_idx: int = 1
    for e in res_entities:
        print_entity(e, s_idx, knowledge_client)
        s_idx += 1
    # All relations
    print('-----------------------------------------------------------------------------------------------------------')
    for r in relations:
        print(f'Subject: {r[0]} Predicate: {r[1]} Object: {r[2]}')
    print('-----------------------------------------------------------------------------------------------------------')
    page_id = None

    # Listing all entities which have the type
    idx: int = 1
    while True:
        # pull
        entities, total_number, next_page_id = knowledge_client.listing(ART_STYLE_CLASS, page_id=page_id, limit=100)
        pulled_entities: int = len(entities)
        entity_count += pulled_entities
        print('-------------------------------------------------------------------------------------------------------')
        print(f' Page: {page_number} Number of entities: {len(entities)}  ({entity_count}/{total_number}) '
              f'Next page id: {next_page_id}')
        print('-------------------------------------------------------------------------------------------------------')
        for e in entities:
            print_entity(e, idx, knowledge_client)
            idx += 1
        if pulled_entities == 0:
            break
        page_number += 1
        page_id = next_page_id
    print()
    # Delete all personal entities for this user
    while True:
        # pull
        entities, total_number, next_page_id = knowledge_client.listing(THING_OBJECT, page_id=page_id,
                                                                        limit=100)
        pulled_entities: int = len(entities)
        if pulled_entities == 0:
            break
        delete_uris: List[str] = [e.uri for e in entities]
        print(f'Cleanup. Delete entities: {delete_uris}')
        knowledge_client.delete_entities(uris=delete_uris, force=True)
        page_number += 1
        page_id = next_page_id
    print('-----------------------------------------------------------------------------------------------------------')
```

### Named Entity Linking 

Performing Named Entity Linking (NEL) on text and Universal Ink Model.

```python
import argparse
from typing import List, Dict

import urllib3

from knowledge.base.language import EN_US
from knowledge.base.ontology import OntologyPropertyReference, ThingObject, ObjectProperty
from knowledge.nel.base import KnowledgeGraphEntity
from knowledge.nel.engine import WacomEntityLinkingEngine
from knowledge.services.graph import WacomKnowledgeService

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


TEXT: str = "Leonardo da Vinci painted the Mona Lisa."


def print_entity(entity: KnowledgeGraphEntity, list_idx: int, auth_key: str, client: WacomKnowledgeService):
    """
    Printing entity details.

    Parameters
    ----------
    entity: KnowledgeGraphEntity
        Named entity
    list_idx: int
        Index with a list
    auth_key: str
        Authorization key
    client: WacomKnowledgeService
        Knowledge graph client
    """
    thing: ThingObject = knowledge_client.entity(auth_key=user_token, uri=entity.entity_source.uri)
    print(f'[{list_idx}] - {entity.ref_text} [{entity.start_idx}-{entity.end_idx}] : {thing.uri}'
          f' <{thing.concept_type.iri}>')
    if len(thing.label) > 0:
        print('    | [Labels]')
        for la in thing.label:
            print(f'    |     |- "{la.content}"@{la.language_code}')
        print('    |')
    if len(thing.label) > 0:
        print('    | [Alias]')
        for la in thing.alias:
            print(f'    |     |- "{la.content}"@{la.language_code}')
        print('    |')
    relations: Dict[OntologyPropertyReference, ObjectProperty] = client.relations(auth_key=auth_key, uri=thing.uri)
    if len(thing.data_properties) > 0:
        print('    | [Attributes]')
        for data_property, labels in thing.data_properties.items():
            print(f'    |    |- {data_property.iri}:')
            for li in labels:
                print(f'    |    |-- "{li.value}"@{li.language_code}')
        print('    |')
    if len(relations) > 0:
        print('    | [Relations]')
        for re in relations.values():
            print(f'    |--- {re.relation.iri}: ')
            print(f'           |- [Incoming]: {re.incoming_relations} ')
            print(f'           |- [Outgoing]: {re.outgoing_relations}')
    print()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default="https://private-knowledge.wacom.com", help="URL of instance")
    args = parser.parse_args()
    TENANT_KEY: str = args.tenant
    EXTERNAL_USER_ID: str = args.user
    # Wacom personal knowledge REST API Client
    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(
        application_name="Named Entity Linking Knowledge access",
        service_url=args.instance)
    #  Wacom Named Entity Linking
    nel_client: WacomEntityLinkingEngine = WacomEntityLinkingEngine(
        service_url=args.instance,
        service_endpoint=WacomEntityLinkingEngine.SERVICE_ENDPOINT
    )
    # Use special tenant for testing:  Unit-test tenant
    user_token, refresh_token, expiration_time = nel_client.request_user_token(TENANT_KEY, EXTERNAL_USER_ID)
    entities: List[KnowledgeGraphEntity] = nel_client.\
        link_personal_entities(text=TEXT, language_code=EN_US, auth_key=user_token)
    idx: int = 1
    print('-----------------------------------------------------------------------------------------------------------')
    print(f'Text: "{TEXT}"@{EN_US}')
    print('-----------------------------------------------------------------------------------------------------------')
    for e in entities:
        print_entity(e, idx, user_token, knowledge_client)
        idx += 1

```

### Access Management

The sample shows, how access to entities can be shared with a group of users or the tenant.

```python
import argparse
from typing import List

from knowledge.base.entity import Label, Description
from knowledge.base.language import EN_US, DE_DE, JA_JP
from knowledge.base.ontology import OntologyClassReference, ThingObject
from knowledge.services.base import WacomServiceException
from knowledge.services.graph import WacomKnowledgeService
from knowledge.services.group import GroupManagementService, Group
from knowledge.services.users import UserManagementServiceAPI

# ------------------------------- User credential ----------------------------------------------------------------------
TOPIC_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Topic')


def create_entity() -> ThingObject:
    """Create a new entity.

    Returns
    -------
    entity: ThingObject
        Entity object
    """
    # Main labels for entity
    topic_labels: List[Label] = [
        Label('Hidden', EN_US),
        Label('Versteckt', DE_DE),
        Label('隠れた', JA_JP),
    ]

    # Topic description
    topic_description: List[Description] = [
        Description('Hidden entity to explain access management.', EN_US),
        Description('Verstecke Entität, um die Zugriffsteuerung zu erlären.', DE_DE)
    ]
    # Topic
    topic_object: ThingObject = ThingObject(label=topic_labels, concept_type=TOPIC_CLASS, description=topic_description)
    return topic_object


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default='https://private-knowledge.wacom.com',
                        help="URL of instance")
    args = parser.parse_args()
    TENANT_KEY: str = args.tenant
    EXTERNAL_USER_ID: str = args.user
    # Wacom personal knowledge REST API Client
    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(application_name="Wacom Knowledge Listing",
                                                                    service_url=args.instance)
    # User Management
    user_management: UserManagementServiceAPI = UserManagementServiceAPI(service_url=args.instance)
    # Group Management
    group_management: GroupManagementService = GroupManagementService(service_url=args.instance)
    admin_token, refresh_token, expiration_time = user_management.request_user_token(TENANT_KEY, EXTERNAL_USER_ID)
    # Now, we create a users
    u1, u1_token, _, _ = user_management.create_user(TENANT_KEY, "u1")
    u2, u2_token, _, _ = user_management.create_user(TENANT_KEY, "u2")
    u3, u3_token, _, _ = user_management.create_user(TENANT_KEY, "u3")

    # Now, let's create an entity
    thing: ThingObject = create_entity()
    entity_uri: str = knowledge_client.create_entity(thing, auth_key=u1_token)
    # Only user 1 can access the entity from cloud storage
    my_thing: ThingObject = knowledge_client.entity(entity_uri, auth_key=u1_token)
    print(f'User is the owner of {my_thing.owner}')
    # Now only user 1 has access to the personal entity
    knowledge_client.entity(entity_uri, auth_key=u1_token)
    # Try to access the entity
    try:
        knowledge_client.entity(entity_uri, auth_key=u2_token)
    except WacomServiceException as we:
        print(f"Expected exception as user 2 has no access to the personal entity of user 1. Exception: {we}")
        print(f"Status code: {we.status_code}")
        print(f"Response text: {we.service_response}")
    # Try to access the entity
    try:
        knowledge_client.entity(entity_uri, auth_key=u3_token)
    except WacomServiceException as we:
        print(f"Expected exception as user 3 has no access to the personal entity of user 1. Exception: {we}")
    # Now, user 1 creates a group
    g: Group = group_management.create_group("test-group", auth_key=u1_token)
    # Shares the join key with user 2 and user 2 joins
    group_management.join_group(g.id, g.join_key, auth_key=u2_token)
    # Share entity with group
    group_management.add_entity_to_group(g.id, entity_uri, auth_key=u1_token)
    # Now, user 2 should have access
    other_thing: ThingObject = knowledge_client.entity(entity_uri, auth_key=u2_token)
    print(f'User 2 is the owner of the thing: {other_thing.owner}')
    # Try to access the entity
    try:
        knowledge_client.entity(entity_uri, auth_key=u3_token)
    except WacomServiceException as we:
        print(f"Expected exception as user 3 still has no access to the personal entity of user 1. Exception: {we}")
        print(f"URL: {we.url}, method: {we.method}")
        print(f"Status code: {we.status_code}")
        print(f"Response text: {we.service_response}")
        print(f"Message: {we.message}")
    # Un-share the entity
    group_management.remove_entity_to_group(g.id, entity_uri, auth_key=u1_token)
    # Now, again no access
    try:
        knowledge_client.entity(entity_uri, auth_key=u2_token)
    except WacomServiceException as we:
        print(f"Expected exception as user 2 has no access to the personal entity of user 1. Exception: {we}")
        print(f"URL: {we.url}, method: {we.method}")
        print(f"Status code: {we.status_code}")
        print(f"Response text: {we.service_response}")
        print(f"Message: {we.message}")
    group_management.leave_group(group_id=g.id, auth_key=u2_token)
    # Now, share the entity with the whole tenant
    my_thing.tenant_access_right.read = True
    knowledge_client.update_entity(my_thing, auth_key=u1_token)
    # Now, all users can access the entity
    knowledge_client.entity(entity_uri, auth_key=u2_token)
    knowledge_client.entity(entity_uri, auth_key=u3_token)
    # Finally, clean up
    knowledge_client.delete_entity(entity_uri, force=True, auth_key=u1_token)
    # Remove users
    user_management.delete_user(TENANT_KEY, u1.external_user_id, u1.id, force=True)
    user_management.delete_user(TENANT_KEY, u2.external_user_id, u2.id, force=True)
    user_management.delete_user(TENANT_KEY, u3.external_user_id, u3.id, force=True)

```

### Ontology Creation

The samples show how the ontology can be extended and new entities can be added using the added classes.

```python
import argparse
import sys
from typing import Optional, List

from knowledge.base.entity import Label, Description
from knowledge.base.language import EN_US, DE_DE
from knowledge.base.ontology import DataPropertyType, OntologyClassReference, OntologyPropertyReference, ThingObject, \
    DataProperty, OntologyContext
from knowledge.services.graph import WacomKnowledgeService
from knowledge.services.ontology import OntologyService
from knowledge.services.session import PermanentSession

# ------------------------------- Constants ----------------------------------------------------------------------------
LEONARDO_DA_VINCI: str = 'Leonardo da Vinci'
CONTEXT_NAME: str = 'core'
# Wacom Base Ontology Types
PERSON_TYPE: OntologyClassReference = OntologyClassReference.parse("wacom:core#Person")
# Demo Class
ARTIST_TYPE: OntologyClassReference = OntologyClassReference.parse("demo:creative#Artist")
# Demo Object property
IS_INSPIRED_BY: OntologyPropertyReference = OntologyPropertyReference.parse("demo:creative#isInspiredBy")
# Demo Data property
STAGE_NAME: OntologyPropertyReference = OntologyPropertyReference.parse("demo:creative#stageName")


def create_artist() -> ThingObject:
    """
    Create a new artist entity.
    Returns
    -------
    instance: ThingObject
        Artist entity
    """
    # Main labels for entity
    topic_labels: List[Label] = [
        Label('Gian Giacomo Caprotti', EN_US),
    ]

    # Topic description
    topic_description: List[Description] = [
        Description('Hidden entity to explain access management.', EN_US),
        Description('Verstecke Entität, um die Zugriffsteuerung zu erlären.', DE_DE)
    ]

    data_property: DataProperty = DataProperty(content='Salaj',
                                               property_ref=STAGE_NAME,
                                               language_code=EN_US)
    # Topic
    artist: ThingObject = ThingObject(label=topic_labels, concept_type=ARTIST_TYPE, description=topic_description)
    artist.add_data_property(data_property)
    return artist


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default="https://private-knowledge.wacom.com", help="URL of instance")
    args = parser.parse_args()
    TENANT_KEY: str = args.tenant
    EXTERNAL_USER_ID: str = args.user
    # Wacom Ontology REST API Client
    ontology_client: OntologyService = OntologyService(service_url=args.instance)
    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(
        application_name="Ontology Creation Demo",
        service_url=args.instance)
    # Login as admin user
    session: PermanentSession = ontology_client.login(TENANT_KEY, EXTERNAL_USER_ID)
    if session.roles != "TenantAdmin":
        print(f'User {EXTERNAL_USER_ID} is not an admin user.')
        sys.exit(1)
    knowledge_client.use_session(session.id)
    knowledge_client.ontology_update()
    context: Optional[OntologyContext] = ontology_client.context()
    if context is None:
        # First, create a context for the ontology
        ontology_client.create_context(name=CONTEXT_NAME, base_uri=f'demo:{CONTEXT_NAME}')
        context_name: str = CONTEXT_NAME
    else:
        context_name: str = context.context
    # Creating a class which is a subclass of a person
    ontology_client.create_concept(context_name, reference=ARTIST_TYPE, subclass_of=PERSON_TYPE)

    # Object properties
    ontology_client.create_object_property(context=context_name, reference=IS_INSPIRED_BY, domains_cls=[ARTIST_TYPE],
                                           ranges_cls=[PERSON_TYPE], inverse_of=None, subproperty_of=None)
    # Data properties
    ontology_client.create_data_property(context=context_name, reference=STAGE_NAME,
                                         domains_cls=[ARTIST_TYPE],
                                         ranges_cls=[DataPropertyType.STRING],
                                         subproperty_of=None)
    # Commit the changes of the ontology. This is very important to confirm changes.
    ontology_client.commit(context=context_name)
    # Trigger graph service. After the update the ontology is available and the new entities can be created
    knowledge_client.ontology_update()

    res_entities, next_search_page = knowledge_client.search_labels(search_term=LEONARDO_DA_VINCI,
                                                                    language_code=EN_US, limit=1000)
    leo: Optional[ThingObject] = None
    for entity in res_entities:
        #  Entity must be a person and the label match with full string
        if entity.concept_type == PERSON_TYPE and LEONARDO_DA_VINCI in [la.content for la in entity.label]:
            leo = entity
            break

    artist_student: ThingObject = create_artist()
    artist_student_uri: str = knowledge_client.create_entity(artist_student)
    knowledge_client.create_relation(artist_student_uri, IS_INSPIRED_BY, leo.uri)

```

### Asynchronous Client 

The sample shows how to use the asynchronous client. 
Most of the methods are available in the asynchronous client(s).
Only for the ontology management the asynchronous client is not available.

```python
import argparse
import asyncio
import uuid
from pathlib import Path
from typing import Tuple, List, Dict, Any, Optional

from knowledge.base.entity import Label
from knowledge.base.language import LanguageCode, EN, SUPPORTED_LOCALES, EN_US
from knowledge.base.ontology import ThingObject
from knowledge.ontomapping import load_configuration
from knowledge.ontomapping.manager import wikidata_to_thing
from knowledge.public.relations import wikidata_relations_extractor
from knowledge.public.wikidata import WikidataSearchResult, WikiDataAPIClient, WikidataThing
from knowledge.services.asyncio.graph import AsyncWacomKnowledgeService
from knowledge.services.asyncio.group import AsyncGroupManagementService
from knowledge.services.asyncio.users import AsyncUserManagementService
from knowledge.services.base import WacomServiceException, format_exception
from knowledge.services.group import Group
from knowledge.services.session import PermanentSession, RefreshableSession
from knowledge.services.users import UserRole, User


def import_entity_from_wikidata(search_term: str, locale: LanguageCode) -> Dict[str, ThingObject]:
    """
    Import entity from Wikidata.
    Parameters
    ----------
    search_term: str
        Search term
    locale: LanguageCode
        Language code

    Returns
    -------
    things: Dict[str, ThingObject]
        Mapping qid to thing object
    """
    search_results: List[WikidataSearchResult] = WikiDataAPIClient.search_term(search_term, locale)
    # Load mapping configuration
    load_configuration(Path(__file__).parent.parent / 'pkl-cache' / 'ontology_mapping.json')
    # Search wikidata for entities
    qid_entities: List[WikidataThing] = WikiDataAPIClient.retrieve_entities([sr.qid for sr in search_results])
    qid_things: Dict[str, WikidataThing] = {qt.qid: qt for qt in qid_entities}
    relations: Dict[str, List[Dict[str, Any]]] = wikidata_relations_extractor(qid_things)
    # Now, let's create the things
    things: Dict[str, ThingObject] = {}
    for res in qid_entities:
        wikidata_thing, import_warnings = wikidata_to_thing(res, all_relations=relations,
                                                            supported_locales=SUPPORTED_LOCALES,
                                                            pull_wikipedia=True,
                                                            all_wikidata_objects=qid_things)
        things[res.qid] = wikidata_thing
    return things


async def user_management_sample(tenant_api_key: str, instance: str) -> Tuple[User, str, str]:
    """
    User management sample.
    Parameters
    ----------
    tenant_api_key: str
        Session
    instance: str
        Instance URL

    Returns
    -------
    user: User
        User object
    user_token: str
        User token
    refresh_token: str
        Refresh token
    """
    user_management: AsyncUserManagementService = AsyncUserManagementService(
                                                    application_name="Async user management sample",
                                                    service_url=instance)
    meta_data: dict = {'user-type': 'demo'}
    user, user_token, refresh_token, _ = await user_management.create_user(tenant_key=tenant_api_key,
                                                                           external_id=uuid.uuid4().hex,
                                                                           meta_data=meta_data,
                                                                           roles=[UserRole.USER])
    return user, user_token, refresh_token


async def clean_up(instance: str, tenant_api_key: str):
    """
    Clean up sample.
    Parameters
    ----------
    instance: str
        Instance URL
    tenant_api_key: str
        Tenant API key
    """
    user_management: AsyncUserManagementService = AsyncUserManagementService(
                                                    application_name="Async user management sample",
                                                    service_url=instance)
    users: List[User] = await user_management.listing_users(tenant_api_key)
    for user in users:
        if 'user-type' in user.meta_data and user.meta_data['user-type'] == 'demo':
            await user_management.delete_user(tenant_key=tenant_api_key, external_id=user.external_user_id,
                                              internal_id=user.id, force=True)


async def main(external_user_id: str, tenant_api_key: str, instance: str):
    """
    Main function for the async sample.

    Parameters
    ----------
    external_user_id: str
        External Id of the shadow user within the Wacom Personal Knowledge.
    tenant_api_key: str
        Tenant api key of the shadow user within the Wacom Personal Knowledge.
    instance: str
        URL of instance
    """
    async_client: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name="Async sample",
                                                                          service_url=instance)
    permanent_session: PermanentSession = await async_client.login(tenant_api_key=tenant_api_key,
                                                                   external_user_id=external_user_id)
    """
    The permanent session contains the external user id, the tenant id, thus it is capable to refresh the token and 
    re-login if needed. The functions check if the token is expired and refresh it if needed. Internally, the token 
    manager handles the session. There are three different session types:
    - Permanent session: The session is refreshed automatically if needed.
    - Refreshable session: The session is not refreshed automatically using the refresh token, 
                           but if the session is not used for a day the refresh token is invalidated.
    - Timed session: The session is only has the authentication token and no refresh token. Thus, it times out after
                     one hour.
    """
    print(f'Service instance: {async_client.service_url}')
    print('-' * 100)
    print(f'Logged in as {permanent_session.external_user_id} (tenant id: {permanent_session.tenant_id}) ')
    is_ten_admin: bool = permanent_session.roles == "TenantAdmin"
    print(f'Is tenant admin: {is_ten_admin}')
    print('-' * 100)
    print(f'Token information')
    print('-' * 100)
    print(f'Refreshable: {permanent_session.refreshable}')
    print(f'Token must be refreshed before: {permanent_session.expiration} UTC')
    print(f'Token expires in {permanent_session.expires_in} seconds)')
    print('-' * 100)
    print(f'Creating two users')
    print('-' * 100)
    # User management sample
    user_1, user_token_1, refresh_token_1 = await user_management_sample(tenant_api_key, instance)
    print(f'User: {user_1}')
    user_2, user_token_2, refresh_token_2 = await user_management_sample(tenant_api_key, instance)
    print(f'User: {user_2}')
    print('-' * 100)
    async_client_user_1: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name="Async user 1",
                                                                                 service_url=instance)
    refresh_session_1: RefreshableSession = await async_client_user_1.register_token(auth_key=user_token_1,
                                                                                     refresh_token=refresh_token_1)
    async_client_user_2: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name="Async sample",
                                                                                 service_url=instance)
    await async_client_user_2.register_token(auth_key=user_token_2, refresh_token=refresh_token_2)
    """
    Now, let's create some entities.
    """
    print('Creation of entities')
    print('-' * 100)
    things_objects: Dict[str, ThingObject] = import_entity_from_wikidata('Leonardo da Vinci', EN)
    created: List[ThingObject] = await async_client_user_1.create_entity_bulk(list(things_objects.values()))
    for thing in created:
        try:
            await async_client_user_2.entity(thing.uri)
        except WacomServiceException as we:
            print(f'User 2 cannot see entity {thing.uri}.\n{format_exception(we)}')

    # Now using the group management service
    group_management: AsyncGroupManagementService = AsyncGroupManagementService(application_name="Group management",
                                                                                service_url=instance)
    await group_management.use_session(refresh_session_1.id)
    # User 1 creates a group
    new_group: Group = await group_management.create_group("sample-group")
    for thing in created:
        try:
            await group_management.add_entity_to_group(new_group.id, thing.uri)
        except WacomServiceException as we:
            print(f'User 1 cannot delete entity {thing.uri}.\n{format_exception(we)}')
    await group_management.add_user_to_group(new_group.id, user_2.id)
    print(f'User 2 can see the entities now. Let us check with async client 2. '
          f'Id of the user: {async_client_user_2.current_session.external_user_id}')
    for thing in created:
        iter_thing: ThingObject = await async_client_user_2.entity(thing.uri)
        label: Optional[Label] = iter_thing.label_lang(EN_US)
        print(f'User 2 can see entity {label.content if label else "UNKNOWN"} {iter_thing.uri}.'
              f'Ownership: owner flag:={iter_thing.owner}, owner is {iter_thing.owner_id}.')
    print('-' * 100)
    await clean_up(instance=instance, tenant_api_key=tenant_api_key)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default='https://private-knowledge.wacom.com',
                        help="URL of instance")
    args = parser.parse_args()
    asyncio.run(main(args.user, args.tenant, args.instance))
```
### Semantic Search

The sample shows how to use the semantic search.
There are two types of search:
- Label search
- Document search

The label search is used to find entities based on the label.
The document search is used to find documents based on the content.


```python
import argparse
import re
import time
from typing import List, Dict, Any

from knowledge.base.language import EN_US
from knowledge.base.search import LabelMatchingResponse, DocumentSearchResponse, VectorDBDocument
from knowledge.services.search import SemanticSearchClient


def clean_text(text: str, max_length: int = -1) -> str:
    """
    Clean text from new lines and multiple spaces.

    Parameters
    ----------
    text: str
        Text to clean.
    max_length: int [default=-1]
        Maximum length of the cleaned text. If length is - 1 then the text is not truncated.

    Returns
    -------
    str
        Cleaned text.
    """
    # First remove new lines
    text = text.strip().replace('\n', ' ')
    # Then remove multiple spaces
    text = re.sub(r'\s+', ' ', text)
    if 0 < max_length < len(text):
        return text[:max_length] + '...'
    return text


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="External Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-t", "--tenant", help="Tenant Id of the shadow user within the Wacom Personal Knowledge.",
                        required=True)
    parser.add_argument("-i", "--instance", default="https://private-knowledge.wacom.com", help="URL of instance")
    args = parser.parse_args()
    client: SemanticSearchClient = SemanticSearchClient(service_url=args.instance)
    session = client.login(args.tenant, args.user)
    max_results: int = 10
    labels_count: int = client.count_documents(locale=EN_US)
    print(f"Tenant ID: {client.current_session.tenant_id} | Labels count: {labels_count} for [locale:={EN_US}]")
    t0: float = time.time()
    results: LabelMatchingResponse = client.labels_search(query="Leonardo Da Vinci", locale=EN_US,
                                                          max_results=max_results)
    t1: float = time.time()
    if len(results.results) > 0:
        print("=" * 120)
        for idx, res in enumerate(results.results):
            print(f"{idx + 1}. {res.label} | Relevance: ({res.score:.2f}) | URI: {res.entity_uri}")
        all_labels: List[VectorDBDocument] = client.retrieve_labels(EN_US, results.results[0].entity_uri)
        print("=" * 120)
        print(f"Labels for best match: {results.results[0].entity_uri}")
        for idx, label in enumerate(all_labels):
            print(f"{idx + 1}. {label.content}")
    print("=" * 120)
    print(f"Time: {(t1 - t0) * 1000:.2f} ms")
    print("=" * 120)
    document_count: int = client.count_documents(locale=EN_US)
    print(f"Document count: {document_count} for [locale:={EN_US}]")
    t2: float = time.time()
    document_results: DocumentSearchResponse = client.document_search(query="Leonardo Da Vinci artwork", locale=EN_US,
                                                                      max_results=max_results)
    t3: float = time.time()
    print("=" * 120)
    if len(document_results.results) > 0:

        for idx, res in enumerate(document_results.results):
            print(f"{idx + 1}.  URI: {res.content_uri} | Relevance: {res.score:.2f} | Chunk:"
                  f"\n\t{clean_text(res.content_chunk, max_length=100)}")
        print(f"\n All document chunks for best match: {document_results.results[0].content_uri}")
        print("=" * 120)
        # If you need all document chunks, you can retrieve them using the content_uri.
        best_match_uri: str = document_results.results[0].content_uri
        chunks: List[VectorDBDocument] = client.retrieve_documents_chunks(locale=EN_US, uri=best_match_uri)
        metadata: Dict[str, Any] = document_results.results[0].metadata
        for idx, chunk in enumerate(chunks):
            print(f"{idx + 1}. {clean_text(chunk.content)}")
        print("\n\tMetadata:\n\t---------")
        for key, value in metadata.items():
            print(f"\t- {key}: {clean_text(value, max_length=100) if isinstance(value, str) else value }")
    print("=" * 120)
    print(f"Time: {(t3 - t2) * 1000:.2f} ms")
    print("=" * 120)
```


## Tools

The following samples show how to utilize the library to work with Wacom's Personal Knowledge.

### Tenant creation

The script `setup_tenant.py` creates a tenant with a user and an ontology. 
It takes a JSON configuration file as input. Here's an example of a configuration file:

```json
{
  "service": {
    "url": "<URL-OF-DEPLOYMENT>"
  },
  "tenant": {
    "api_key": "<TENANT-API-KEY>"
  },
  "users": [
    {
      "external_id": "<ADMIN-EXTERNAL-ID>",
      "role": "tenantadmin",
      "meta_data": {
        "user_type": "system"
      }
    },
    {
      "external_id": "<USER-EXTERNAL-ID>",
      "role": "user",
      "meta_data": {
        "user_type": "system",
        "description": "This is a user created by the system"
      }
    }
  ],
  "ontology": {
    "context": "<CORE-NAME>",
    "schema": "<SCHEMA>"
  }
}
```

This JSON configuration is designed to set up a tenant for a private knowledge system. 
It specifies the service URL, tenant API key, users, and initial ontology for the knowledge graph. 
Here's a breakdown of its structure:

1. **service**: This section contains information about the service deployment.
   - `url`: The URL of the deployment.

2. **tenant**: This section includes details specific to the tenant.
   - `api_key`: The API key for the tenant.

3. **users**: This array holds information about users who will have access to the tenant. Each user has the following fields:
   - `external_id`: A unique identifier for the user.
   - `role`: Defines the user's role within the tenant. Possible roles include "tenantadmin" for administrative access and "user" for general user access.
   - `meta_data`: Additional information about the user. 
     - `user_type`: Specifies the type of user, e.g., "system".
     - `description` (optional): Provides a description of the user, particularly useful for system-created users.

4. **ontology**: This section defines the initial setup of the knowledge graph.
   - `context`: The context of the ontology, e.g., "core".
   - `schema`: Specifies the schema to be used, e.g., "company".


### Listing script

Listing the entities for tenant. 

```bash
>> python listing.py [-h] -u USER -t TENANT [-r] [-i INSTANCE]
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge 
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-r, --relations (optional)_ -  Requesting the relations for each entity

### Export entities script

Dump all entities of a user to a ndjson file. 

```bash
>> python export_entities.py [-h] -u USER -t TENANT [-r] [-a] [-p] [-d DUMP]
                              [-c CONCEPT_TYPE] [-i INSTANCE]
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance. (default:=https://private-
                        knowledge.wacom.com)
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge 
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-r, --relations (optional)_ -  Requesting the relations for each entity
- _-a, --all (optional)_ - All entities the user as access to, otherwise only his own entities are dumped.
- _-p, --images (optional)_ - Include the images in the dump.
- _-d DUMP, --dump DUMP_ -  Defines the location of the dump path.
 
### Import entities script

Pushing entities to knowledge graph.

```bash
>> python import_entities.py [-h] [-u USER] [-t TENANT] [-g GROUP_NAME] [-r] [-i INSTANCE]
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge 
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-i CACHE, --cache CACHE_ - Path to entities that must be imported.
- _-g GROUP_NAME, --group_id GROUP_NAME_ - Group name where the entities will be assigned to. 
- _-p , --public_ - Group name where the entities will be assigned to.

### Wikidata scrapper

```bash
usage: wikidata_scrapper.py [-h] -u USER -t TENANT [-i INSTANCE] [-c CACHE]
                            [-m MAPPING] [-d DEPTH]
                            [-l LANGUAGES [LANGUAGES ...]]
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-c CACHE, --cache CACHE_ - Path to entities that must are exports in import format.
- _-m MAPPING, --mapping MAPPING_ - Mapping file to configure the wikidata mapping.
- _-d DEPTH, --depth DEPTH_ - Depth of the graph to be scrapped.
- _-l LANGUAGES [LANGUAGES ...], --languages LANGUAGES [LANGUAGES ...]_ - Languages to be scrapped.

### Wikidata search 

```bash
usage: wikidata_search.py [-h] [-t TERM] [-l LANGUAGE]
```

**Parameters:**

- _-t TERM, --term TERM_ - Search term.
- _-l LANGUAGE, --language LANGUAGE_ - Language code

### Semantic Label Search

```bash
usage: semantic_search_labels.py [-h] -u USER -t TENANT [-i INSTANCE] -q QUERY
                                 [-l LOCALE] -m MAX_RESULTS
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-q QUERY, --query QUERY_ - Search term
- _-l LOCALE, --locale LOCALE_ - Language code
- _-m MAX_RESULTS, --max_results MAX_RESULTS_ - Maximum number of results

### Semantic Document Search

```bash
usage: semantic_search_documents.py [-h] -u USER -t TENANT [-i INSTANCE] -q
                                    QUERY [-l LOCALE] -m MAX_RESULTS
```

**Parameters:**

- _-i INSTANCE, --instance INSTANCE_ - URL of instance
- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge
- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant
- _-q QUERY, --query QUERY_ - Search term
- _-l LOCALE, --locale LOCALE_ - Language code
- _-m MAX_RESULTS, --max_results MAX_RESULTS_ - Maximum number of results

# Documentation

You can find more detailed technical documentation, [here](https://developer-docs.wacom.com/preview/semantic-ink/).
API documentation is available [here](./docs/).

## Contributing
Contribution guidelines are still work in progress.

## License
[Apache License 2.0](LICENSE)



            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/Wacom-Developer/personal-knowledge-library",
    "name": "personal-knowledge-library",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "semantic-knowledge;knowledge-graph",
    "author": "Markus Weber",
    "author_email": "markus.weber@wacom.com",
    "download_url": "https://files.pythonhosted.org/packages/38/9f/e5b89f3a5355216cc7b006d4890dce3a5fff275e7eacfd812c27a154bab2/personal_knowledge_library-2.4.3.tar.gz",
    "platform": null,
    "description": "# Wacom Private Knowledge Library\n\n[![Python package](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/python-package.yml/badge.svg)](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/python-package.yml)\n[![Pylint](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/pylint.yml/badge.svg)](https://github.com/Wacom-Developer/personal-knowledge-library/actions/workflows/pylint.yml)\n\n![License: Apache 2](https://img.shields.io/badge/License-Apache2-green.svg)\n[![PyPI](https://img.shields.io/pypi/v/personal-knowledge-library.svg)](https://pypi.python.org/pypi/personal-knowledge-library)\n[![PyPI](https://img.shields.io/pypi/pyversions/personal-knowledge-library.svg)](https://pypi.python.org/pypi/personal-knowledge-library)\n[![Documentation](https://img.shields.io/badge/api-reference-blue.svg)](https://developer-docs.wacom.com/docs/private-knowledge-service) \n\n![Contributors](https://img.shields.io/github/contributors/Wacom-Developer/personal-knowledge-library.svg)\n![GitHub forks](https://img.shields.io/github/forks/Wacom-Developer/personal-knowledge-library.svg)\n![GitHub stars](https://img.shields.io/github/stars/Wacom-Developer/personal-knowledge-library.svg)\n\nThe required tenant API key is only available for selected partner companies.\nPlease contact your Wacom representative for more information.\n\n## Introduction\n\nIn knowledge management there is a distinction between data, information and knowledge.\nIn the domain of digital ink this means:\n\n- **Data** - The equivalent would be the ink strokes\n- **Information** - After using handwriting-, shape-, math-, or other recognition processes ink strokes are converted into machine readable content, such as text, shapes, math representations, other other digital content\n- **Knowledge / Semantics** -  Beyond recognition content needs to be semantically analysed to become semantically understood based on a shared common knowledge.\n\nThe following illustration shows the different layers of knowledge:\n![Levels of ink knowledge layers](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/knowledge-levels.png)\n\nFor handling semantics, Wacom introduced the Wacom Private Knowledge (WPK) cloud service to manage personal ontologies and its associated personal knowledge graph.\n\nThis library provide simplified access to Wacom's personal knowledge cloud service.\nIt contains:\n\n- Basic datastructures for Ontology object and entities from the knowledge graph\n- Clients for the REST APIs\n- Connector for Wikidata public knowledge graph\n\n**Ontology service:**\n\n- List all Ontology structures\n- Modify Ontology structures\n- Delete Ontology structures\n\n**Entity service:**\n\n- List all entities\n- Add entities to knowledge graph\n- Access object properties\n\n**Search service:**\n\n- Search for entities for labels and descriptions with a given language\n- Search for literals (data properties) \n- Search for relations (object properties)\n\n**Group service:**\n\n- List all groups\n- Add groups, modify groups, delete groups\n- Add users and entities to groups\n\n**Ontology service:**\n\n- List all Ontology structures\n- Modify Ontology structures\n\n**Named Entity Linking service:**\n\n- Linking words to knowledge entities from graph in a given text (Ontology-based Named Entity Linking)\n\n**Wikidata connector:**\n\n- Import entities from Wikidata\n- Mapping Wikidata entities to WPK entities\n\n# Technology stack\n\n## Domain Knowledge\n\nThe tasks of the ontology within Wacom's private knowledge system is to formalised the domain the technology is used in, such as education-, smart home-, or creative domein.\nThe domain model will be the foundation for the entities collected within the knowledge graph, describing real world concepts in a formal language understood by artificial intelligence system:\n\n- Foundation for structured data, knowledge representation as concepts and relations among concepts\n- Being explicit definitions of shared vocabularies for interoperability\n- Being actionable fragments of explicit knowledge that engines can use for inferencing (Reasoning)\n- Can be used for problem solving\n\nAn ontology defines (specifies) the concepts, relationships, and other distinctions that are relevant for modelling a domain.\n\n## Knowledge Graph\n\n- Knowledge graph is generated from unstructured and structured knowledge sources\n- Contains all structured knowledge gathered from all sources\n- Foundation for all semantic algorithms\n\n## Semantic Technology\n\n- Extract knowledge from various sources (Connectors)\n- Linking words to knowledge entities from graph in a given text (Ontology-based Named Entity Linking)\n- Enables a smart search functionality which understands the context and finds related documents (Semantic Search)\n\n\n# Functionality\n\n## Import Format\n\nFor importing entities into the knowledge graph, the tools/import_entities.py script can be used.\n\nThe ThingObject support a NDJSON based import format, where the individual JSON files can contain the following structure.\n\n| Field name             | Subfield name | Data Structure | Description                                                                                    |\n|------------------------|---------------|----------------|------------------------------------------------------------------------------------------------|\n| source_reference_id    |               | str            | A unique identifier for the entity used in the source system                                  |\n| source_system          |               | str            | The source system describes the original source of the entity, such as wikidata, youtube, ... |\n| image                  |               | str            | A string representing the URL of the entity's icon.                                           |\n| labels                 |               | array          | An array of label objects, where each object has the following fields:                       |\n|                        | value         | str            | A string representing the label text in the specified locale.                                |\n|                        | locale        | str            | A string combining the ISO-3166 country code and the ISO-639 language code (e.g., \"en-US\").  |\n|                        | isMain        | bool           | A boolean flag indicating if this label is the main label for the entity (true) or an alias (false). |\n| descriptions           |               | array          | An array of description objects, where each object has the following fields:                 |\n|                        | description   | str            | A string representing the description text in the specified locale.                          |\n|                        | locale        | str            | A string combining the ISO-3166 country code and the ISO-639 language code (e.g., \"en-US\").  |\n| type                   |               | str            | A string representing the IRI of the ontology class for this entity.                         |\n| literals               |               | array[map]     | An array of data property objects, where each object has the following fields:               |\n\n\n## Access API\n\nThe personal knowledge graph backend is implement as a multi-tenancy system.\nThus, several tenants can be logically separated from each other and different organisations can build their one knowledge graph.\n\n![Tenant concept](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/tenant-concept.png)\n\nIn general, a tenant with their users, groups, and entities are logically separated.\nPhysically the entities are store in the same instance of the Wacom Private Knowledge (WPK) backend database system.\n\nThe user management is rather limited, each organisation must provide their own authentication service and user management.\nThe backend only has a reference of the user (*\u201cshadow user\u201d*) by an **external user id**.\n\nThe management of tenants is limited to the system owner - Wacom -, as it requires a **tenant management API** key.\nWhile users for each tenant can be created by the owner of the **Tenant API Key**.\nYou will receive this token from the system owner after the creation of the tenant.\n\n\n> :warning: Store the **Tenant API Key** in a secure key store, as attackers can use the key to harm your system.\n\n\nThe **Tenant API Key** should be only used by your authentication service to create shadow users and to login your user into the WPK backend.\nAfter a successful user login, you will receive a token which can be used by the user to create, update, or delete entities and relations.\n\nThe following illustration summarizes the flows for creation of tenant and users:\n\n![Tenant and user creation](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/tenant-user-creation.png)\n\nThe organisation itself needs to implement their own authentication service which:\n\n- handles the users and their passwords,\n- controls the personal data of the users,\n- connects the users with the WPK backend and share with them the user token.\n\nThe WPK backend only manages the access levels of the entities and the group management for users.\nThe illustration shows how the access token is received from the WPK endpoint:\n\n![Access token request.](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/access-token.png)\n\n# Entity API\n\nThe entities used within the knowledge graph and the relationship among them is defined within an ontology that is manage with Wacom Ontology Management System (WOMS).\n\nAn entity within the personal knowledge graphs consist of these major parts:\n\n- **Icon** - a visual representation of the entity, for instance a portrait of a person.\n- **URI** - a unique resource identifier of an entity in the graph.\n- **Type** - the type links to the defined concept class in the ontology.\n- **Labels** - labels are the word(s) use in a language for the concept.\n- **Description** - a short abstract that describes the entity.\n- **Literals** - literals are properties of an entity, such as first name of a person. The ontology defines all literals of the concept class as well as its data type.\n- **Relations** - the relationship among different entities is described using relations.\n\nThe following illustration provides an example for an entity:\n\n![Entity description](https://github.com/Wacom-Developer/personal-knowledge-library/blob/main/assets/entity-description.png)\n\n## Entity content\n\nEntities in general are language-independent as across nationalities or cultures we only use different scripts and words for a shared instance of a concept.\n\nLet's take Leonardo da Vinci as an example.\nThe ontology defines the concept of a Person, a human being.\nNow, in English its label would be _Leonardo da Vinci_, while in Japanese _\u30ec\u30aa\u30ca\u30eb\u30c9\u30fb\u30c0\u30fb\u30f4\u30a3\u30f3\u30c1_.\nMoreover, he is also known as _Leonardo di ser Piero da Vinci_ or _\u30c0\u30fb\u30d3\u30f3\u30c1_.\n\n### Labels\n\nNow, in the given example all words that a assigned to the concept are labels.\nThe label _Leonardo da Vinci_ is stored in the backend with an additional language code, e.g. _en_.\n\nThere is always a main label, which refers to the most common or official name of entity.\nAnother example would be Wacom, where _Wacom Co., Ltd._ is the official name while _Wacom_ is commonly used and be considered as an alias.\n\n>  :pushpin: For the language code the **ISO 639-1:2002**, codes for the representation of names of languages\u2014Part 1: Alpha-2 code. Read more, [here](https://www.iso.org/standard/22109.html)\n\n## Samples\n\n### Entity handling\n\nThis samples shows how to work with graph service.\n\n```python\nimport argparse\nfrom typing import Optional, Dict, List\n\nfrom knowledge.base.entity import Description, Label\nfrom knowledge.base.language import LocaleCode, EN_US, DE_DE\nfrom knowledge.base.ontology import OntologyClassReference, OntologyPropertyReference, ThingObject, ObjectProperty\nfrom knowledge.services.graph import WacomKnowledgeService\n\n# ------------------------------- Knowledge entities -------------------------------------------------------------------\nLEONARDO_DA_VINCI: str = 'Leonardo da Vinci'\nSELF_PORTRAIT_STYLE: str = 'self-portrait'\nICON: str = \"https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/Mona_Lisa_%28copy%2C_Thalwil%2C_Switzerland%29.\"\\\n            \"JPG/1024px-Mona_Lisa_%28copy%2C_Thalwil%2C_Switzerland%29.JPG\"\n# ------------------------------- Ontology class names -----------------------------------------------------------------\nTHING_OBJECT: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Thing')\n\"\"\"\nThe Ontology will contain a Thing class where is the root class in the hierarchy. \n\"\"\"\nARTWORK_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'creative', 'VisualArtwork')\nPERSON_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Person')\nART_STYLE_CLASS: OntologyClassReference = OntologyClassReference.parse('wacom:creative#ArtStyle')\nIS_CREATOR: OntologyPropertyReference = OntologyPropertyReference('wacom', 'core', 'created')\nHAS_TOPIC: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:core#hasTopic')\nCREATED: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:core#created')\nHAS_ART_STYLE: OntologyPropertyReference = OntologyPropertyReference.parse('wacom:creative#hasArtstyle')\n\n\ndef print_entity(display_entity: ThingObject, list_idx: int, client: WacomKnowledgeService,\n                 short: bool = False):\n    \"\"\"\n    Printing entity details.\n\n    Parameters\n    ----------\n    display_entity: ThingObject\n        Entity with properties\n    list_idx: int\n        Index with a list\n    client: WacomKnowledgeService\n        Knowledge graph client\n    short: bool\n        Short summary\n    \"\"\"\n    print(f'[{list_idx}] : {display_entity.uri} <{display_entity.concept_type.iri}>')\n    if len(display_entity.label) > 0:\n        print('    | [Labels]')\n        for la in display_entity.label:\n            print(f'    |     |- \"{la.content}\"@{la.language_code}')\n        print('    |')\n    if not short:\n        if len(display_entity.alias) > 0:\n            print('    | [Alias]')\n            for la in display_entity.alias:\n                print(f'    |     |- \"{la.content}\"@{la.language_code}')\n            print('    |')\n        if len(display_entity.data_properties) > 0:\n            print('    | [Attributes]')\n            for data_property, labels in display_entity.data_properties.items():\n                print(f'    |    |- {data_property.iri}:')\n                for li in labels:\n                    print(f'    |    |-- \"{li.value}\"@{li.language_code}')\n            print('    |')\n\n        relations_obj: Dict[OntologyPropertyReference, ObjectProperty] = client.relations(uri=display_entity.uri)\n        if len(relations_obj) > 0:\n            print('    | [Relations]')\n            for r_idx, re in enumerate(relations_obj.values()):\n                last: bool = r_idx == len(relations_obj) - 1\n                print(f'    |--- {re.relation.iri}: ')\n                print(f'    {\"|\" if not last else \" \"}       |- [Incoming]: {re.incoming_relations} ')\n                print(f'    {\"|\" if not last else \" \"}       |- [Outgoing]: {re.outgoing_relations}')\n        print()\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default='https://private-knowledge.wacom.com',\n                        help=\"URL of instance\")\n    args = parser.parse_args()\n    TENANT_KEY: str = args.tenant\n    EXTERNAL_USER_ID: str = args.user\n    # Wacom personal knowledge REST API Client\n    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(application_name=\"Wacom Knowledge Listing\",\n                                                                    service_url=args.instance)\n    knowledge_client.login(args.tenant, args.user)\n    page_id: Optional[str] = None\n    page_number: int = 1\n    entity_count: int = 0\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(' First step: Find Leonardo da Vinci in the knowledge graph.')\n    print('-----------------------------------------------------------------------------------------------------------')\n    res_entities, next_search_page = knowledge_client.search_labels(search_term=LEONARDO_DA_VINCI,\n                                                                    language_code=LocaleCode('en_US'), limit=1000)\n    leo: Optional[ThingObject] = None\n    s_idx: int = 1\n    for res_entity in res_entities:\n        #  Entity must be a person and the label match with full string\n        if res_entity.concept_type == PERSON_CLASS and LEONARDO_DA_VINCI in [la.content for la in res_entity.label]:\n            leo = res_entity\n            break\n\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(' What artwork exists in the knowledge graph.')\n    print('-----------------------------------------------------------------------------------------------------------')\n    relations_dict: Dict[OntologyPropertyReference, ObjectProperty] = knowledge_client.relations(uri=leo.uri)\n    print(f' Artwork of {leo.label}')\n    print('-----------------------------------------------------------------------------------------------------------')\n    idx: int = 1\n    if CREATED in relations_dict:\n        for e in relations_dict[CREATED].outgoing_relations:\n            print(f' [{idx}] {e.uri}: {e.label}')\n            idx += 1\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(' Let us create a new piece of artwork.')\n    print('-----------------------------------------------------------------------------------------------------------')\n\n    # Main labels for entity\n    artwork_labels: List[Label] = [\n        Label('Ginevra Gherardini', EN_US),\n        Label('Ginevra Gherardini', DE_DE)\n    ]\n    # Alias labels for entity\n    artwork_alias: List[Label] = [\n        Label(\"Ginevra\", EN_US),\n        Label(\"Ginevra\", DE_DE)\n    ]\n    # Topic description\n    artwork_description: List[Description] = [\n        Description('Oil painting of Mona Lisa\\' sister', EN_US),\n        Description('\u00d6lgem\u00e4lde von Mona Lisa\\' Schwester', DE_DE)\n    ]\n    # Topic\n    artwork_object: ThingObject = ThingObject(label=artwork_labels, concept_type=ARTWORK_CLASS,\n                                              description=artwork_description,\n                                              icon=ICON)\n    artwork_object.alias = artwork_alias\n    print(f' Create: {artwork_object}')\n    # Create artwork\n    artwork_entity_uri: str = knowledge_client.create_entity(artwork_object)\n    print(f' Entity URI: {artwork_entity_uri}')\n    # Create relation between Leonardo da Vinci and artwork\n    knowledge_client.create_relation(source=leo.uri, relation=IS_CREATOR, target=artwork_entity_uri)\n\n    relations_dict = knowledge_client.relations(uri=artwork_entity_uri)\n    for ontology_property, object_property in relations_dict.items():\n        print(f'  {object_property}')\n    # You will see that wacom:core#isCreatedBy is automatically inferred as relation as it is the inverse property of\n    # wacom:core#created.\n\n    # Now, more search options\n    res_entities, next_search_page = knowledge_client.search_description('Michelangelo\\'s Sistine Chapel',\n                                                                         EN_US, limit=1000)\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(' Search results.  Description: \"Michelangelo\\'s Sistine Chapel\"')\n    print('-----------------------------------------------------------------------------------------------------------')\n    s_idx: int = 1\n    for e in res_entities:\n        print_entity(e, s_idx, knowledge_client)\n\n    # Now, let's search all artwork that has the art style self-portrait\n    res_entities, next_search_page = knowledge_client.search_labels(search_term=SELF_PORTRAIT_STYLE,\n                                                                    language_code=EN_US, limit=1000)\n    art_style: Optional[ThingObject] = None\n    s_idx: int = 1\n    for entity in res_entities:\n        #  Entity must be a person and the label match with full string\n        if entity.concept_type == ART_STYLE_CLASS and SELF_PORTRAIT_STYLE in [la.content for la in entity.label]:\n            art_style = entity\n            break\n    res_entities, next_search_page = knowledge_client.search_relation(subject_uri=None,\n                                                                      relation=HAS_ART_STYLE,\n                                                                      object_uri=art_style.uri,\n                                                                      language_code=EN_US)\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(' Search results.  Relation: relation:=has_topic  object_uri:= unknown')\n    print('-----------------------------------------------------------------------------------------------------------')\n    s_idx: int = 1\n    for e in res_entities:\n        print_entity(e, s_idx, knowledge_client, short=True)\n        s_idx += 1\n\n    # Finally, the activation function retrieving the related identities to a pre-defined depth.\n    entities, relations = knowledge_client.activations(uris=[leo.uri], depth=1)\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(f'Activation.  URI: {leo.uri}')\n    print('-----------------------------------------------------------------------------------------------------------')\n    s_idx: int = 1\n    for e in res_entities:\n        print_entity(e, s_idx, knowledge_client)\n        s_idx += 1\n    # All relations\n    print('-----------------------------------------------------------------------------------------------------------')\n    for r in relations:\n        print(f'Subject: {r[0]} Predicate: {r[1]} Object: {r[2]}')\n    print('-----------------------------------------------------------------------------------------------------------')\n    page_id = None\n\n    # Listing all entities which have the type\n    idx: int = 1\n    while True:\n        # pull\n        entities, total_number, next_page_id = knowledge_client.listing(ART_STYLE_CLASS, page_id=page_id, limit=100)\n        pulled_entities: int = len(entities)\n        entity_count += pulled_entities\n        print('-------------------------------------------------------------------------------------------------------')\n        print(f' Page: {page_number} Number of entities: {len(entities)}  ({entity_count}/{total_number}) '\n              f'Next page id: {next_page_id}')\n        print('-------------------------------------------------------------------------------------------------------')\n        for e in entities:\n            print_entity(e, idx, knowledge_client)\n            idx += 1\n        if pulled_entities == 0:\n            break\n        page_number += 1\n        page_id = next_page_id\n    print()\n    # Delete all personal entities for this user\n    while True:\n        # pull\n        entities, total_number, next_page_id = knowledge_client.listing(THING_OBJECT, page_id=page_id,\n                                                                        limit=100)\n        pulled_entities: int = len(entities)\n        if pulled_entities == 0:\n            break\n        delete_uris: List[str] = [e.uri for e in entities]\n        print(f'Cleanup. Delete entities: {delete_uris}')\n        knowledge_client.delete_entities(uris=delete_uris, force=True)\n        page_number += 1\n        page_id = next_page_id\n    print('-----------------------------------------------------------------------------------------------------------')\n```\n\n### Named Entity Linking \n\nPerforming Named Entity Linking (NEL) on text and Universal Ink Model.\n\n```python\nimport argparse\nfrom typing import List, Dict\n\nimport urllib3\n\nfrom knowledge.base.language import EN_US\nfrom knowledge.base.ontology import OntologyPropertyReference, ThingObject, ObjectProperty\nfrom knowledge.nel.base import KnowledgeGraphEntity\nfrom knowledge.nel.engine import WacomEntityLinkingEngine\nfrom knowledge.services.graph import WacomKnowledgeService\n\nurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n\nTEXT: str = \"Leonardo da Vinci painted the Mona Lisa.\"\n\n\ndef print_entity(entity: KnowledgeGraphEntity, list_idx: int, auth_key: str, client: WacomKnowledgeService):\n    \"\"\"\n    Printing entity details.\n\n    Parameters\n    ----------\n    entity: KnowledgeGraphEntity\n        Named entity\n    list_idx: int\n        Index with a list\n    auth_key: str\n        Authorization key\n    client: WacomKnowledgeService\n        Knowledge graph client\n    \"\"\"\n    thing: ThingObject = knowledge_client.entity(auth_key=user_token, uri=entity.entity_source.uri)\n    print(f'[{list_idx}] - {entity.ref_text} [{entity.start_idx}-{entity.end_idx}] : {thing.uri}'\n          f' <{thing.concept_type.iri}>')\n    if len(thing.label) > 0:\n        print('    | [Labels]')\n        for la in thing.label:\n            print(f'    |     |- \"{la.content}\"@{la.language_code}')\n        print('    |')\n    if len(thing.label) > 0:\n        print('    | [Alias]')\n        for la in thing.alias:\n            print(f'    |     |- \"{la.content}\"@{la.language_code}')\n        print('    |')\n    relations: Dict[OntologyPropertyReference, ObjectProperty] = client.relations(auth_key=auth_key, uri=thing.uri)\n    if len(thing.data_properties) > 0:\n        print('    | [Attributes]')\n        for data_property, labels in thing.data_properties.items():\n            print(f'    |    |- {data_property.iri}:')\n            for li in labels:\n                print(f'    |    |-- \"{li.value}\"@{li.language_code}')\n        print('    |')\n    if len(relations) > 0:\n        print('    | [Relations]')\n        for re in relations.values():\n            print(f'    |--- {re.relation.iri}: ')\n            print(f'           |- [Incoming]: {re.incoming_relations} ')\n            print(f'           |- [Outgoing]: {re.outgoing_relations}')\n    print()\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default=\"https://private-knowledge.wacom.com\", help=\"URL of instance\")\n    args = parser.parse_args()\n    TENANT_KEY: str = args.tenant\n    EXTERNAL_USER_ID: str = args.user\n    # Wacom personal knowledge REST API Client\n    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(\n        application_name=\"Named Entity Linking Knowledge access\",\n        service_url=args.instance)\n    #  Wacom Named Entity Linking\n    nel_client: WacomEntityLinkingEngine = WacomEntityLinkingEngine(\n        service_url=args.instance,\n        service_endpoint=WacomEntityLinkingEngine.SERVICE_ENDPOINT\n    )\n    # Use special tenant for testing:  Unit-test tenant\n    user_token, refresh_token, expiration_time = nel_client.request_user_token(TENANT_KEY, EXTERNAL_USER_ID)\n    entities: List[KnowledgeGraphEntity] = nel_client.\\\n        link_personal_entities(text=TEXT, language_code=EN_US, auth_key=user_token)\n    idx: int = 1\n    print('-----------------------------------------------------------------------------------------------------------')\n    print(f'Text: \"{TEXT}\"@{EN_US}')\n    print('-----------------------------------------------------------------------------------------------------------')\n    for e in entities:\n        print_entity(e, idx, user_token, knowledge_client)\n        idx += 1\n\n```\n\n### Access Management\n\nThe sample shows, how access to entities can be shared with a group of users or the tenant.\n\n```python\nimport argparse\nfrom typing import List\n\nfrom knowledge.base.entity import Label, Description\nfrom knowledge.base.language import EN_US, DE_DE, JA_JP\nfrom knowledge.base.ontology import OntologyClassReference, ThingObject\nfrom knowledge.services.base import WacomServiceException\nfrom knowledge.services.graph import WacomKnowledgeService\nfrom knowledge.services.group import GroupManagementService, Group\nfrom knowledge.services.users import UserManagementServiceAPI\n\n# ------------------------------- User credential ----------------------------------------------------------------------\nTOPIC_CLASS: OntologyClassReference = OntologyClassReference('wacom', 'core', 'Topic')\n\n\ndef create_entity() -> ThingObject:\n    \"\"\"Create a new entity.\n\n    Returns\n    -------\n    entity: ThingObject\n        Entity object\n    \"\"\"\n    # Main labels for entity\n    topic_labels: List[Label] = [\n        Label('Hidden', EN_US),\n        Label('Versteckt', DE_DE),\n        Label('\u96a0\u308c\u305f', JA_JP),\n    ]\n\n    # Topic description\n    topic_description: List[Description] = [\n        Description('Hidden entity to explain access management.', EN_US),\n        Description('Verstecke Entit\u00e4t, um die Zugriffsteuerung zu erl\u00e4ren.', DE_DE)\n    ]\n    # Topic\n    topic_object: ThingObject = ThingObject(label=topic_labels, concept_type=TOPIC_CLASS, description=topic_description)\n    return topic_object\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default='https://private-knowledge.wacom.com',\n                        help=\"URL of instance\")\n    args = parser.parse_args()\n    TENANT_KEY: str = args.tenant\n    EXTERNAL_USER_ID: str = args.user\n    # Wacom personal knowledge REST API Client\n    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(application_name=\"Wacom Knowledge Listing\",\n                                                                    service_url=args.instance)\n    # User Management\n    user_management: UserManagementServiceAPI = UserManagementServiceAPI(service_url=args.instance)\n    # Group Management\n    group_management: GroupManagementService = GroupManagementService(service_url=args.instance)\n    admin_token, refresh_token, expiration_time = user_management.request_user_token(TENANT_KEY, EXTERNAL_USER_ID)\n    # Now, we create a users\n    u1, u1_token, _, _ = user_management.create_user(TENANT_KEY, \"u1\")\n    u2, u2_token, _, _ = user_management.create_user(TENANT_KEY, \"u2\")\n    u3, u3_token, _, _ = user_management.create_user(TENANT_KEY, \"u3\")\n\n    # Now, let's create an entity\n    thing: ThingObject = create_entity()\n    entity_uri: str = knowledge_client.create_entity(thing, auth_key=u1_token)\n    # Only user 1 can access the entity from cloud storage\n    my_thing: ThingObject = knowledge_client.entity(entity_uri, auth_key=u1_token)\n    print(f'User is the owner of {my_thing.owner}')\n    # Now only user 1 has access to the personal entity\n    knowledge_client.entity(entity_uri, auth_key=u1_token)\n    # Try to access the entity\n    try:\n        knowledge_client.entity(entity_uri, auth_key=u2_token)\n    except WacomServiceException as we:\n        print(f\"Expected exception as user 2 has no access to the personal entity of user 1. Exception: {we}\")\n        print(f\"Status code: {we.status_code}\")\n        print(f\"Response text: {we.service_response}\")\n    # Try to access the entity\n    try:\n        knowledge_client.entity(entity_uri, auth_key=u3_token)\n    except WacomServiceException as we:\n        print(f\"Expected exception as user 3 has no access to the personal entity of user 1. Exception: {we}\")\n    # Now, user 1 creates a group\n    g: Group = group_management.create_group(\"test-group\", auth_key=u1_token)\n    # Shares the join key with user 2 and user 2 joins\n    group_management.join_group(g.id, g.join_key, auth_key=u2_token)\n    # Share entity with group\n    group_management.add_entity_to_group(g.id, entity_uri, auth_key=u1_token)\n    # Now, user 2 should have access\n    other_thing: ThingObject = knowledge_client.entity(entity_uri, auth_key=u2_token)\n    print(f'User 2 is the owner of the thing: {other_thing.owner}')\n    # Try to access the entity\n    try:\n        knowledge_client.entity(entity_uri, auth_key=u3_token)\n    except WacomServiceException as we:\n        print(f\"Expected exception as user 3 still has no access to the personal entity of user 1. Exception: {we}\")\n        print(f\"URL: {we.url}, method: {we.method}\")\n        print(f\"Status code: {we.status_code}\")\n        print(f\"Response text: {we.service_response}\")\n        print(f\"Message: {we.message}\")\n    # Un-share the entity\n    group_management.remove_entity_to_group(g.id, entity_uri, auth_key=u1_token)\n    # Now, again no access\n    try:\n        knowledge_client.entity(entity_uri, auth_key=u2_token)\n    except WacomServiceException as we:\n        print(f\"Expected exception as user 2 has no access to the personal entity of user 1. Exception: {we}\")\n        print(f\"URL: {we.url}, method: {we.method}\")\n        print(f\"Status code: {we.status_code}\")\n        print(f\"Response text: {we.service_response}\")\n        print(f\"Message: {we.message}\")\n    group_management.leave_group(group_id=g.id, auth_key=u2_token)\n    # Now, share the entity with the whole tenant\n    my_thing.tenant_access_right.read = True\n    knowledge_client.update_entity(my_thing, auth_key=u1_token)\n    # Now, all users can access the entity\n    knowledge_client.entity(entity_uri, auth_key=u2_token)\n    knowledge_client.entity(entity_uri, auth_key=u3_token)\n    # Finally, clean up\n    knowledge_client.delete_entity(entity_uri, force=True, auth_key=u1_token)\n    # Remove users\n    user_management.delete_user(TENANT_KEY, u1.external_user_id, u1.id, force=True)\n    user_management.delete_user(TENANT_KEY, u2.external_user_id, u2.id, force=True)\n    user_management.delete_user(TENANT_KEY, u3.external_user_id, u3.id, force=True)\n\n```\n\n### Ontology Creation\n\nThe samples show how the ontology can be extended and new entities can be added using the added classes.\n\n```python\nimport argparse\nimport sys\nfrom typing import Optional, List\n\nfrom knowledge.base.entity import Label, Description\nfrom knowledge.base.language import EN_US, DE_DE\nfrom knowledge.base.ontology import DataPropertyType, OntologyClassReference, OntologyPropertyReference, ThingObject, \\\n    DataProperty, OntologyContext\nfrom knowledge.services.graph import WacomKnowledgeService\nfrom knowledge.services.ontology import OntologyService\nfrom knowledge.services.session import PermanentSession\n\n# ------------------------------- Constants ----------------------------------------------------------------------------\nLEONARDO_DA_VINCI: str = 'Leonardo da Vinci'\nCONTEXT_NAME: str = 'core'\n# Wacom Base Ontology Types\nPERSON_TYPE: OntologyClassReference = OntologyClassReference.parse(\"wacom:core#Person\")\n# Demo Class\nARTIST_TYPE: OntologyClassReference = OntologyClassReference.parse(\"demo:creative#Artist\")\n# Demo Object property\nIS_INSPIRED_BY: OntologyPropertyReference = OntologyPropertyReference.parse(\"demo:creative#isInspiredBy\")\n# Demo Data property\nSTAGE_NAME: OntologyPropertyReference = OntologyPropertyReference.parse(\"demo:creative#stageName\")\n\n\ndef create_artist() -> ThingObject:\n    \"\"\"\n    Create a new artist entity.\n    Returns\n    -------\n    instance: ThingObject\n        Artist entity\n    \"\"\"\n    # Main labels for entity\n    topic_labels: List[Label] = [\n        Label('Gian Giacomo Caprotti', EN_US),\n    ]\n\n    # Topic description\n    topic_description: List[Description] = [\n        Description('Hidden entity to explain access management.', EN_US),\n        Description('Verstecke Entit\u00e4t, um die Zugriffsteuerung zu erl\u00e4ren.', DE_DE)\n    ]\n\n    data_property: DataProperty = DataProperty(content='Salaj',\n                                               property_ref=STAGE_NAME,\n                                               language_code=EN_US)\n    # Topic\n    artist: ThingObject = ThingObject(label=topic_labels, concept_type=ARTIST_TYPE, description=topic_description)\n    artist.add_data_property(data_property)\n    return artist\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default=\"https://private-knowledge.wacom.com\", help=\"URL of instance\")\n    args = parser.parse_args()\n    TENANT_KEY: str = args.tenant\n    EXTERNAL_USER_ID: str = args.user\n    # Wacom Ontology REST API Client\n    ontology_client: OntologyService = OntologyService(service_url=args.instance)\n    knowledge_client: WacomKnowledgeService = WacomKnowledgeService(\n        application_name=\"Ontology Creation Demo\",\n        service_url=args.instance)\n    # Login as admin user\n    session: PermanentSession = ontology_client.login(TENANT_KEY, EXTERNAL_USER_ID)\n    if session.roles != \"TenantAdmin\":\n        print(f'User {EXTERNAL_USER_ID} is not an admin user.')\n        sys.exit(1)\n    knowledge_client.use_session(session.id)\n    knowledge_client.ontology_update()\n    context: Optional[OntologyContext] = ontology_client.context()\n    if context is None:\n        # First, create a context for the ontology\n        ontology_client.create_context(name=CONTEXT_NAME, base_uri=f'demo:{CONTEXT_NAME}')\n        context_name: str = CONTEXT_NAME\n    else:\n        context_name: str = context.context\n    # Creating a class which is a subclass of a person\n    ontology_client.create_concept(context_name, reference=ARTIST_TYPE, subclass_of=PERSON_TYPE)\n\n    # Object properties\n    ontology_client.create_object_property(context=context_name, reference=IS_INSPIRED_BY, domains_cls=[ARTIST_TYPE],\n                                           ranges_cls=[PERSON_TYPE], inverse_of=None, subproperty_of=None)\n    # Data properties\n    ontology_client.create_data_property(context=context_name, reference=STAGE_NAME,\n                                         domains_cls=[ARTIST_TYPE],\n                                         ranges_cls=[DataPropertyType.STRING],\n                                         subproperty_of=None)\n    # Commit the changes of the ontology. This is very important to confirm changes.\n    ontology_client.commit(context=context_name)\n    # Trigger graph service. After the update the ontology is available and the new entities can be created\n    knowledge_client.ontology_update()\n\n    res_entities, next_search_page = knowledge_client.search_labels(search_term=LEONARDO_DA_VINCI,\n                                                                    language_code=EN_US, limit=1000)\n    leo: Optional[ThingObject] = None\n    for entity in res_entities:\n        #  Entity must be a person and the label match with full string\n        if entity.concept_type == PERSON_TYPE and LEONARDO_DA_VINCI in [la.content for la in entity.label]:\n            leo = entity\n            break\n\n    artist_student: ThingObject = create_artist()\n    artist_student_uri: str = knowledge_client.create_entity(artist_student)\n    knowledge_client.create_relation(artist_student_uri, IS_INSPIRED_BY, leo.uri)\n\n```\n\n### Asynchronous Client \n\nThe sample shows how to use the asynchronous client. \nMost of the methods are available in the asynchronous client(s).\nOnly for the ontology management the asynchronous client is not available.\n\n```python\nimport argparse\nimport asyncio\nimport uuid\nfrom pathlib import Path\nfrom typing import Tuple, List, Dict, Any, Optional\n\nfrom knowledge.base.entity import Label\nfrom knowledge.base.language import LanguageCode, EN, SUPPORTED_LOCALES, EN_US\nfrom knowledge.base.ontology import ThingObject\nfrom knowledge.ontomapping import load_configuration\nfrom knowledge.ontomapping.manager import wikidata_to_thing\nfrom knowledge.public.relations import wikidata_relations_extractor\nfrom knowledge.public.wikidata import WikidataSearchResult, WikiDataAPIClient, WikidataThing\nfrom knowledge.services.asyncio.graph import AsyncWacomKnowledgeService\nfrom knowledge.services.asyncio.group import AsyncGroupManagementService\nfrom knowledge.services.asyncio.users import AsyncUserManagementService\nfrom knowledge.services.base import WacomServiceException, format_exception\nfrom knowledge.services.group import Group\nfrom knowledge.services.session import PermanentSession, RefreshableSession\nfrom knowledge.services.users import UserRole, User\n\n\ndef import_entity_from_wikidata(search_term: str, locale: LanguageCode) -> Dict[str, ThingObject]:\n    \"\"\"\n    Import entity from Wikidata.\n    Parameters\n    ----------\n    search_term: str\n        Search term\n    locale: LanguageCode\n        Language code\n\n    Returns\n    -------\n    things: Dict[str, ThingObject]\n        Mapping qid to thing object\n    \"\"\"\n    search_results: List[WikidataSearchResult] = WikiDataAPIClient.search_term(search_term, locale)\n    # Load mapping configuration\n    load_configuration(Path(__file__).parent.parent / 'pkl-cache' / 'ontology_mapping.json')\n    # Search wikidata for entities\n    qid_entities: List[WikidataThing] = WikiDataAPIClient.retrieve_entities([sr.qid for sr in search_results])\n    qid_things: Dict[str, WikidataThing] = {qt.qid: qt for qt in qid_entities}\n    relations: Dict[str, List[Dict[str, Any]]] = wikidata_relations_extractor(qid_things)\n    # Now, let's create the things\n    things: Dict[str, ThingObject] = {}\n    for res in qid_entities:\n        wikidata_thing, import_warnings = wikidata_to_thing(res, all_relations=relations,\n                                                            supported_locales=SUPPORTED_LOCALES,\n                                                            pull_wikipedia=True,\n                                                            all_wikidata_objects=qid_things)\n        things[res.qid] = wikidata_thing\n    return things\n\n\nasync def user_management_sample(tenant_api_key: str, instance: str) -> Tuple[User, str, str]:\n    \"\"\"\n    User management sample.\n    Parameters\n    ----------\n    tenant_api_key: str\n        Session\n    instance: str\n        Instance URL\n\n    Returns\n    -------\n    user: User\n        User object\n    user_token: str\n        User token\n    refresh_token: str\n        Refresh token\n    \"\"\"\n    user_management: AsyncUserManagementService = AsyncUserManagementService(\n                                                    application_name=\"Async user management sample\",\n                                                    service_url=instance)\n    meta_data: dict = {'user-type': 'demo'}\n    user, user_token, refresh_token, _ = await user_management.create_user(tenant_key=tenant_api_key,\n                                                                           external_id=uuid.uuid4().hex,\n                                                                           meta_data=meta_data,\n                                                                           roles=[UserRole.USER])\n    return user, user_token, refresh_token\n\n\nasync def clean_up(instance: str, tenant_api_key: str):\n    \"\"\"\n    Clean up sample.\n    Parameters\n    ----------\n    instance: str\n        Instance URL\n    tenant_api_key: str\n        Tenant API key\n    \"\"\"\n    user_management: AsyncUserManagementService = AsyncUserManagementService(\n                                                    application_name=\"Async user management sample\",\n                                                    service_url=instance)\n    users: List[User] = await user_management.listing_users(tenant_api_key)\n    for user in users:\n        if 'user-type' in user.meta_data and user.meta_data['user-type'] == 'demo':\n            await user_management.delete_user(tenant_key=tenant_api_key, external_id=user.external_user_id,\n                                              internal_id=user.id, force=True)\n\n\nasync def main(external_user_id: str, tenant_api_key: str, instance: str):\n    \"\"\"\n    Main function for the async sample.\n\n    Parameters\n    ----------\n    external_user_id: str\n        External Id of the shadow user within the Wacom Personal Knowledge.\n    tenant_api_key: str\n        Tenant api key of the shadow user within the Wacom Personal Knowledge.\n    instance: str\n        URL of instance\n    \"\"\"\n    async_client: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name=\"Async sample\",\n                                                                          service_url=instance)\n    permanent_session: PermanentSession = await async_client.login(tenant_api_key=tenant_api_key,\n                                                                   external_user_id=external_user_id)\n    \"\"\"\n    The permanent session contains the external user id, the tenant id, thus it is capable to refresh the token and \n    re-login if needed. The functions check if the token is expired and refresh it if needed. Internally, the token \n    manager handles the session. There are three different session types:\n    - Permanent session: The session is refreshed automatically if needed.\n    - Refreshable session: The session is not refreshed automatically using the refresh token, \n                           but if the session is not used for a day the refresh token is invalidated.\n    - Timed session: The session is only has the authentication token and no refresh token. Thus, it times out after\n                     one hour.\n    \"\"\"\n    print(f'Service instance: {async_client.service_url}')\n    print('-' * 100)\n    print(f'Logged in as {permanent_session.external_user_id} (tenant id: {permanent_session.tenant_id}) ')\n    is_ten_admin: bool = permanent_session.roles == \"TenantAdmin\"\n    print(f'Is tenant admin: {is_ten_admin}')\n    print('-' * 100)\n    print(f'Token information')\n    print('-' * 100)\n    print(f'Refreshable: {permanent_session.refreshable}')\n    print(f'Token must be refreshed before: {permanent_session.expiration} UTC')\n    print(f'Token expires in {permanent_session.expires_in} seconds)')\n    print('-' * 100)\n    print(f'Creating two users')\n    print('-' * 100)\n    # User management sample\n    user_1, user_token_1, refresh_token_1 = await user_management_sample(tenant_api_key, instance)\n    print(f'User: {user_1}')\n    user_2, user_token_2, refresh_token_2 = await user_management_sample(tenant_api_key, instance)\n    print(f'User: {user_2}')\n    print('-' * 100)\n    async_client_user_1: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name=\"Async user 1\",\n                                                                                 service_url=instance)\n    refresh_session_1: RefreshableSession = await async_client_user_1.register_token(auth_key=user_token_1,\n                                                                                     refresh_token=refresh_token_1)\n    async_client_user_2: AsyncWacomKnowledgeService = AsyncWacomKnowledgeService(application_name=\"Async sample\",\n                                                                                 service_url=instance)\n    await async_client_user_2.register_token(auth_key=user_token_2, refresh_token=refresh_token_2)\n    \"\"\"\n    Now, let's create some entities.\n    \"\"\"\n    print('Creation of entities')\n    print('-' * 100)\n    things_objects: Dict[str, ThingObject] = import_entity_from_wikidata('Leonardo da Vinci', EN)\n    created: List[ThingObject] = await async_client_user_1.create_entity_bulk(list(things_objects.values()))\n    for thing in created:\n        try:\n            await async_client_user_2.entity(thing.uri)\n        except WacomServiceException as we:\n            print(f'User 2 cannot see entity {thing.uri}.\\n{format_exception(we)}')\n\n    # Now using the group management service\n    group_management: AsyncGroupManagementService = AsyncGroupManagementService(application_name=\"Group management\",\n                                                                                service_url=instance)\n    await group_management.use_session(refresh_session_1.id)\n    # User 1 creates a group\n    new_group: Group = await group_management.create_group(\"sample-group\")\n    for thing in created:\n        try:\n            await group_management.add_entity_to_group(new_group.id, thing.uri)\n        except WacomServiceException as we:\n            print(f'User 1 cannot delete entity {thing.uri}.\\n{format_exception(we)}')\n    await group_management.add_user_to_group(new_group.id, user_2.id)\n    print(f'User 2 can see the entities now. Let us check with async client 2. '\n          f'Id of the user: {async_client_user_2.current_session.external_user_id}')\n    for thing in created:\n        iter_thing: ThingObject = await async_client_user_2.entity(thing.uri)\n        label: Optional[Label] = iter_thing.label_lang(EN_US)\n        print(f'User 2 can see entity {label.content if label else \"UNKNOWN\"} {iter_thing.uri}.'\n              f'Ownership: owner flag:={iter_thing.owner}, owner is {iter_thing.owner_id}.')\n    print('-' * 100)\n    await clean_up(instance=instance, tenant_api_key=tenant_api_key)\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default='https://private-knowledge.wacom.com',\n                        help=\"URL of instance\")\n    args = parser.parse_args()\n    asyncio.run(main(args.user, args.tenant, args.instance))\n```\n### Semantic Search\n\nThe sample shows how to use the semantic search.\nThere are two types of search:\n- Label search\n- Document search\n\nThe label search is used to find entities based on the label.\nThe document search is used to find documents based on the content.\n\n\n```python\nimport argparse\nimport re\nimport time\nfrom typing import List, Dict, Any\n\nfrom knowledge.base.language import EN_US\nfrom knowledge.base.search import LabelMatchingResponse, DocumentSearchResponse, VectorDBDocument\nfrom knowledge.services.search import SemanticSearchClient\n\n\ndef clean_text(text: str, max_length: int = -1) -> str:\n    \"\"\"\n    Clean text from new lines and multiple spaces.\n\n    Parameters\n    ----------\n    text: str\n        Text to clean.\n    max_length: int [default=-1]\n        Maximum length of the cleaned text. If length is - 1 then the text is not truncated.\n\n    Returns\n    -------\n    str\n        Cleaned text.\n    \"\"\"\n    # First remove new lines\n    text = text.strip().replace('\\n', ' ')\n    # Then remove multiple spaces\n    text = re.sub(r'\\s+', ' ', text)\n    if 0 < max_length < len(text):\n        return text[:max_length] + '...'\n    return text\n\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-u\", \"--user\", help=\"External Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-t\", \"--tenant\", help=\"Tenant Id of the shadow user within the Wacom Personal Knowledge.\",\n                        required=True)\n    parser.add_argument(\"-i\", \"--instance\", default=\"https://private-knowledge.wacom.com\", help=\"URL of instance\")\n    args = parser.parse_args()\n    client: SemanticSearchClient = SemanticSearchClient(service_url=args.instance)\n    session = client.login(args.tenant, args.user)\n    max_results: int = 10\n    labels_count: int = client.count_documents(locale=EN_US)\n    print(f\"Tenant ID: {client.current_session.tenant_id} | Labels count: {labels_count} for [locale:={EN_US}]\")\n    t0: float = time.time()\n    results: LabelMatchingResponse = client.labels_search(query=\"Leonardo Da Vinci\", locale=EN_US,\n                                                          max_results=max_results)\n    t1: float = time.time()\n    if len(results.results) > 0:\n        print(\"=\" * 120)\n        for idx, res in enumerate(results.results):\n            print(f\"{idx + 1}. {res.label} | Relevance: ({res.score:.2f}) | URI: {res.entity_uri}\")\n        all_labels: List[VectorDBDocument] = client.retrieve_labels(EN_US, results.results[0].entity_uri)\n        print(\"=\" * 120)\n        print(f\"Labels for best match: {results.results[0].entity_uri}\")\n        for idx, label in enumerate(all_labels):\n            print(f\"{idx + 1}. {label.content}\")\n    print(\"=\" * 120)\n    print(f\"Time: {(t1 - t0) * 1000:.2f} ms\")\n    print(\"=\" * 120)\n    document_count: int = client.count_documents(locale=EN_US)\n    print(f\"Document count: {document_count} for [locale:={EN_US}]\")\n    t2: float = time.time()\n    document_results: DocumentSearchResponse = client.document_search(query=\"Leonardo Da Vinci artwork\", locale=EN_US,\n                                                                      max_results=max_results)\n    t3: float = time.time()\n    print(\"=\" * 120)\n    if len(document_results.results) > 0:\n\n        for idx, res in enumerate(document_results.results):\n            print(f\"{idx + 1}.  URI: {res.content_uri} | Relevance: {res.score:.2f} | Chunk:\"\n                  f\"\\n\\t{clean_text(res.content_chunk, max_length=100)}\")\n        print(f\"\\n All document chunks for best match: {document_results.results[0].content_uri}\")\n        print(\"=\" * 120)\n        # If you need all document chunks, you can retrieve them using the content_uri.\n        best_match_uri: str = document_results.results[0].content_uri\n        chunks: List[VectorDBDocument] = client.retrieve_documents_chunks(locale=EN_US, uri=best_match_uri)\n        metadata: Dict[str, Any] = document_results.results[0].metadata\n        for idx, chunk in enumerate(chunks):\n            print(f\"{idx + 1}. {clean_text(chunk.content)}\")\n        print(\"\\n\\tMetadata:\\n\\t---------\")\n        for key, value in metadata.items():\n            print(f\"\\t- {key}: {clean_text(value, max_length=100) if isinstance(value, str) else value }\")\n    print(\"=\" * 120)\n    print(f\"Time: {(t3 - t2) * 1000:.2f} ms\")\n    print(\"=\" * 120)\n```\n\n\n## Tools\n\nThe following samples show how to utilize the library to work with Wacom's Personal Knowledge.\n\n### Tenant creation\n\nThe script `setup_tenant.py` creates a tenant with a user and an ontology. \nIt takes a JSON configuration file as input. Here's an example of a configuration file:\n\n```json\n{\n  \"service\": {\n    \"url\": \"<URL-OF-DEPLOYMENT>\"\n  },\n  \"tenant\": {\n    \"api_key\": \"<TENANT-API-KEY>\"\n  },\n  \"users\": [\n    {\n      \"external_id\": \"<ADMIN-EXTERNAL-ID>\",\n      \"role\": \"tenantadmin\",\n      \"meta_data\": {\n        \"user_type\": \"system\"\n      }\n    },\n    {\n      \"external_id\": \"<USER-EXTERNAL-ID>\",\n      \"role\": \"user\",\n      \"meta_data\": {\n        \"user_type\": \"system\",\n        \"description\": \"This is a user created by the system\"\n      }\n    }\n  ],\n  \"ontology\": {\n    \"context\": \"<CORE-NAME>\",\n    \"schema\": \"<SCHEMA>\"\n  }\n}\n```\n\nThis JSON configuration is designed to set up a tenant for a private knowledge system. \nIt specifies the service URL, tenant API key, users, and initial ontology for the knowledge graph. \nHere's a breakdown of its structure:\n\n1. **service**: This section contains information about the service deployment.\n   - `url`: The URL of the deployment.\n\n2. **tenant**: This section includes details specific to the tenant.\n   - `api_key`: The API key for the tenant.\n\n3. **users**: This array holds information about users who will have access to the tenant. Each user has the following fields:\n   - `external_id`: A unique identifier for the user.\n   - `role`: Defines the user's role within the tenant. Possible roles include \"tenantadmin\" for administrative access and \"user\" for general user access.\n   - `meta_data`: Additional information about the user. \n     - `user_type`: Specifies the type of user, e.g., \"system\".\n     - `description` (optional): Provides a description of the user, particularly useful for system-created users.\n\n4. **ontology**: This section defines the initial setup of the knowledge graph.\n   - `context`: The context of the ontology, e.g., \"core\".\n   - `schema`: Specifies the schema to be used, e.g., \"company\".\n\n\n### Listing script\n\nListing the entities for tenant. \n\n```bash\n>> python listing.py [-h] -u USER -t TENANT [-r] [-i INSTANCE]\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge \n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-r, --relations (optional)_ -  Requesting the relations for each entity\n\n### Export entities script\n\nDump all entities of a user to a ndjson file. \n\n```bash\n>> python export_entities.py [-h] -u USER -t TENANT [-r] [-a] [-p] [-d DUMP]\n                              [-c CONCEPT_TYPE] [-i INSTANCE]\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance. (default:=https://private-\n                        knowledge.wacom.com)\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge \n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-r, --relations (optional)_ -  Requesting the relations for each entity\n- _-a, --all (optional)_ - All entities the user as access to, otherwise only his own entities are dumped.\n- _-p, --images (optional)_ - Include the images in the dump.\n- _-d DUMP, --dump DUMP_ -  Defines the location of the dump path.\n \n### Import entities script\n\nPushing entities to knowledge graph.\n\n```bash\n>> python import_entities.py [-h] [-u USER] [-t TENANT] [-g GROUP_NAME] [-r] [-i INSTANCE]\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge \n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-i CACHE, --cache CACHE_ - Path to entities that must be imported.\n- _-g GROUP_NAME, --group_id GROUP_NAME_ - Group name where the entities will be assigned to. \n- _-p , --public_ - Group name where the entities will be assigned to.\n\n### Wikidata scrapper\n\n```bash\nusage: wikidata_scrapper.py [-h] -u USER -t TENANT [-i INSTANCE] [-c CACHE]\n                            [-m MAPPING] [-d DEPTH]\n                            [-l LANGUAGES [LANGUAGES ...]]\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge\n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-c CACHE, --cache CACHE_ - Path to entities that must are exports in import format.\n- _-m MAPPING, --mapping MAPPING_ - Mapping file to configure the wikidata mapping.\n- _-d DEPTH, --depth DEPTH_ - Depth of the graph to be scrapped.\n- _-l LANGUAGES [LANGUAGES ...], --languages LANGUAGES [LANGUAGES ...]_ - Languages to be scrapped.\n\n### Wikidata search \n\n```bash\nusage: wikidata_search.py [-h] [-t TERM] [-l LANGUAGE]\n```\n\n**Parameters:**\n\n- _-t TERM, --term TERM_ - Search term.\n- _-l LANGUAGE, --language LANGUAGE_ - Language code\n\n### Semantic Label Search\n\n```bash\nusage: semantic_search_labels.py [-h] -u USER -t TENANT [-i INSTANCE] -q QUERY\n                                 [-l LOCALE] -m MAX_RESULTS\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge\n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-q QUERY, --query QUERY_ - Search term\n- _-l LOCALE, --locale LOCALE_ - Language code\n- _-m MAX_RESULTS, --max_results MAX_RESULTS_ - Maximum number of results\n\n### Semantic Document Search\n\n```bash\nusage: semantic_search_documents.py [-h] -u USER -t TENANT [-i INSTANCE] -q\n                                    QUERY [-l LOCALE] -m MAX_RESULTS\n```\n\n**Parameters:**\n\n- _-i INSTANCE, --instance INSTANCE_ - URL of instance\n- _-u USER, --user USER_ - External ID to identify user of the Wacom Private Knowledge\n- _-t TENANT, --tenant TENANT_ - Tenant key to identify tenant\n- _-q QUERY, --query QUERY_ - Search term\n- _-l LOCALE, --locale LOCALE_ - Language code\n- _-m MAX_RESULTS, --max_results MAX_RESULTS_ - Maximum number of results\n\n# Documentation\n\nYou can find more detailed technical documentation, [here](https://developer-docs.wacom.com/preview/semantic-ink/).\nAPI documentation is available [here](./docs/).\n\n## Contributing\nContribution guidelines are still work in progress.\n\n## License\n[Apache License 2.0](LICENSE)\n\n\n",
    "bugtrack_url": null,
    "license": "Apache 2.0 License",
    "summary": "Library to access Wacom's Personal Knowledge graph.",
    "version": "2.4.3",
    "project_urls": {
        "Homepage": "https://github.com/Wacom-Developer/personal-knowledge-library"
    },
    "split_keywords": [
        "semantic-knowledge;knowledge-graph"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "1ca24409e09faeed7aa2369db442180e003a89ef1ce33b6aeb4cdee9112b4acb",
                "md5": "ca261809d0c2c58992562da668400280",
                "sha256": "6e7a90c77889cd4a5735951f525c0329239ec53fdcb1e27e84b2797b5c81bf8c"
            },
            "downloads": -1,
            "filename": "personal_knowledge_library-2.4.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "ca261809d0c2c58992562da668400280",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 129808,
            "upload_time": "2024-12-17T07:40:05",
            "upload_time_iso_8601": "2024-12-17T07:40:05.850232Z",
            "url": "https://files.pythonhosted.org/packages/1c/a2/4409e09faeed7aa2369db442180e003a89ef1ce33b6aeb4cdee9112b4acb/personal_knowledge_library-2.4.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "389fe5b89f3a5355216cc7b006d4890dce3a5fff275e7eacfd812c27a154bab2",
                "md5": "9a1c9357ccfbca8589586a8d0126f672",
                "sha256": "231d5973b8e5486dbda44094957f6212f2b9b8534608b00c5c5dbb7ba4af0847"
            },
            "downloads": -1,
            "filename": "personal_knowledge_library-2.4.3.tar.gz",
            "has_sig": false,
            "md5_digest": "9a1c9357ccfbca8589586a8d0126f672",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 136621,
            "upload_time": "2024-12-17T07:40:09",
            "upload_time_iso_8601": "2024-12-17T07:40:09.037599Z",
            "url": "https://files.pythonhosted.org/packages/38/9f/e5b89f3a5355216cc7b006d4890dce3a5fff275e7eacfd812c27a154bab2/personal_knowledge_library-2.4.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-17 07:40:09",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Wacom-Developer",
    "github_project": "personal-knowledge-library",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "lcname": "personal-knowledge-library"
}
        
Elapsed time: 4.31985s