Merge branch 'release/v0.15'

This commit is contained in:
Cyber MacGeddon 2024-11-20 23:15:23 +00:00
commit fd6abdc4c1
149 changed files with 4856 additions and 1274 deletions

20
.github/workflows/pull-request.yaml vendored Normal file
View file

@ -0,0 +1,20 @@
name: Test pull request
on:
pull_request:
permissions:
contents: read
jobs:
container-push:
name: Do nothing
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

86
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,86 @@
name: Build
on:
workflow_dispatch:
push:
tags:
- v0.15.*
permissions:
contents: read
jobs:
deploy:
name: Build everything
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
environment:
name: release
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_SECRET }}
- name: Install build dependencies
run: pip3 install jsonnet
- name: Get version
id: version
run: echo VERSION=$(git describe --exact-match --tags | sed 's/^v//') >> $GITHUB_OUTPUT
- run: echo ${{ steps.version.outputs.VERSION }}
- name: Build packages
run: make packages VERSION=${{ steps.version.outputs.VERSION }}
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Create deploy bundle
run: templates/generate-all deploy.zip ${{ steps.version.outputs.VERSION }}
- uses: ncipollo/release-action@v1
with:
artifacts: deploy.zip
generateReleaseNotes: true
makeLatest: false
prerelease: true
skipIfReleaseExists: true
- name: Build container
run: make container VERSION=${{ steps.version.outputs.VERSION }}
- name: Extract metadata for container
id: meta
uses: docker/metadata-action@v4
with:
images: trustgraph/trustgraph-flow
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push Docker image
id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View file

@ -13,7 +13,7 @@ RUN dnf install -y python3 python3-pip python3-wheel python3-aiohttp \
RUN pip3 install torch --index-url https://download.pytorch.org/whl/cpu
RUN pip3 install anthropic boto3 cohere openai google-cloud-aiplatform ollama \
RUN pip3 install anthropic boto3 cohere openai google-cloud-aiplatform ollama google-generativeai \
langchain langchain-core langchain-huggingface langchain-text-splitters \
langchain-community pymilvus sentence-transformers transformers \
huggingface-hub pulsar-client cassandra-driver pyarrow pyyaml \

View file

@ -1,6 +1,7 @@
# VERSION=$(shell git describe | sed 's/^v//')
VERSION=0.11.20
VERSION=0.0.0
DOCKER=podman
@ -35,6 +36,7 @@ CONTAINER=docker.io/trustgraph/trustgraph-flow
update-package-versions:
mkdir -p trustgraph-cli/trustgraph
mkdir -p trustgraph/trustgraph
echo __version__ = \"${VERSION}\" > trustgraph-base/trustgraph/base_version.py
echo __version__ = \"${VERSION}\" > trustgraph-flow/trustgraph/flow_version.py
echo __version__ = \"${VERSION}\" > trustgraph-vertexai/trustgraph/vertexai_version.py

18
docs/README.agent-demo Normal file
View file

@ -0,0 +1,18 @@
podman-compose -f docker-compose.yaml up -d
tg-processor-state
tg-load-text --keyword cats animals home-life --name "Mark's cats" --description "This document describes Mark's cats" --copyright-notice 'Public domain' --publication-organization 'trustgraph.ai' --publication-date 2024-10-23 --copyright-holder 'trustgraph.ai' --copyright-year 2024 --publication-description 'Uploading to Github' --url https://example.com --id TG-000001 ../trustgraph/README.cats
tg-load-text --keyword nasa challenger space-shuttle shuttle orbiter --name 'Challenger Report Volume 1' --description 'The findings of the Presidential Commission regarding the circumstances surrounding the Challenger accident are reported and recommendations for corrective action are outlined' --copyright-notice 'Work of the US Gov. Public Use Permitted' --publication-organization 'NASA' --publication-date 1986-06-06 --copyright-holder 'US Government' --copyright-year 1986 --publication-description 'The findings of the Commission regarding the circumstances surrounding the Challenger accident are reported' --url https://ntrs.nasa.gov/citations/19860015255 --id AD-A171402 ../trustgraph/README.challenger
tg-graph-show
tg-query-graph-rag -q 'Tell me cat facts'
tg-invoke-agent -v -q "How many cats does Mark have? Calculate that number raised to 0.4 power. Is that number lower than the numeric part of the mission identifier of the Space Shuttle Challenger on its last mission? If so, give me an apple pie recipe, otherwise return a poem about cheese."

35
docs/README.cats Normal file
View file

@ -0,0 +1,35 @@
My name is Mark.
I have 2 cats:
- Fred is a big, fat, orange, stripy cat. He is 12 years old and has 4 legs.
- Hope is a small, black cat. She is 7 years old and also has 4 legs.
Fred has 4 legs.
Hope has 4 legs.
Fred and Hope are nice animals, but occasionally they fight.
Fred is lazy and sleeps a lot. Hope is energetic, runs around a lot and
climbs trees.
Both cats have tails and whiskers like all cats do.
Cats have the species name Felis catus.
The cat (Felis catus), also referred to as domestic cat or house cat, is a
small domesticated carnivorous mammal. It is the only domesticated species of
the family Felidae. Advances in archaeology and genetics have shown that the
domestication of the cat occurred in the Near East around 7500 BC. It is
commonly kept as a pet and farm cat, but also ranges freely as a feral cat
avoiding human contact. Valued by humans for companionship and its ability to
kill vermin, the cat's retractable claws are adapted to killing small prey
like mice and rats. It has a strong, flexible body, quick reflexes, and sharp
teeth, and its night vision and sense of smell are well developed. It is a
social species, but a solitary hunter and a crepuscular predator. Cat
communication includes vocalizations—including meowing, purring, trilling,
hissing, growling, and gruntingas well as body language. It can hear sounds
too faint or too high in frequency for human ears, such as those made by small
mammals. It secretes and perceives pheromones.

54
docs/README.challenger Normal file
View file

@ -0,0 +1,54 @@
On January 28, 1986, the Space Shuttle Challenger broke apart 73 seconds into
its flight, killing all seven crew members aboard. The spacecraft
disintegrated 46,000 feet (14 km) above the Atlantic Ocean, off the coast of
Cape Canaveral, Florida, at 11:39 a.m. EST (16:39 UTC). It was the first fatal
accident involving an American spacecraft while in flight.
The mission, designated STS-51-L, was the 10th flight for the orbiter and the
25th flight of the Space Shuttle fleet. The crew was scheduled to deploy a
communications satellite and study Halley's Comet while they were in orbit, in
addition to taking schoolteacher Christa McAuliffe into space under the
Teacher In Space program. The latter task resulted in a higher-than-usual
media interest in and coverage of the mission; the launch and subsequent
disaster were seen live in many schools across the United States.
The cause of the disaster was the failure of the primary and secondary O-ring
seals in a joint in the shuttle's right solid rocket booster (SRB). The
record-low temperatures on the morning of the launch had stiffened the rubber
O-rings, reducing their ability to seal the joints. Shortly after liftoff, the
seals were breached, and hot pressurized gas from within the SRB leaked
through the joint and burned through the aft attachment strut connecting it to
the external propellant tank (ET), then into the tank itself. The collapse of
the ET's internal structures and the rotation of the SRB that followed threw
the shuttle stack, traveling at a speed of Mach 1.92, into a direction that
allowed aerodynamic forces to tear the orbiter apart. Both SRBs detached from
the now-destroyed ET and continued to fly uncontrollably until the range
safety officer destroyed them.
The crew compartment, human remains, and many other fragments from the shuttle
were recovered from the ocean floor after a three-month search-and-recovery
operation. The exact timing of the deaths of the crew is unknown, but several
crew members are thought to have survived the initial breakup of the
spacecraft. The orbiter had no escape system, and the impact of the crew
compartment at terminal velocity with the ocean surface was too violent to be
survivable.
The disaster resulted in a 32-month hiatus in the Space Shuttle
program. President Ronald Reagan created the Rogers Commission to investigate
the accident. The commission criticized NASA's organizational culture and
decision-making processes that had contributed to the accident. Test data
since 1977 demonstrated a potentially catastrophic flaw in the SRBs' O-rings,
but neither NASA nor SRB manufacturer Morton Thiokol had addressed this known
defect. NASA managers also disregarded engineers' warnings about the dangers
of launching in cold temperatures and did not report these technical concerns
to their superiors.
As a result of this disaster, NASA established the Office of Safety,
Reliability, and Quality Assurance, and arranged for deployment of commercial
satellites from expendable launch vehicles rather than from a crewed
orbiter. To replace Challenger, the construction of a new Space Shuttle
orbiter, Endeavour, was approved in 1987, and the new orbiter first flew in
1992. Subsequent missions were launched with redesigned SRBs and their crews
wore pressurized suits during ascent and reentry.

View file

@ -186,7 +186,7 @@ To change the `Ollama` model, first make sure the desired model has been pulled
### OpenAI API
```
export OPENAI_KEY=<TOKEN-GOES-HERE>
export OPENAI_TOKEN=<TOKEN-GOES-HERE>
docker compose -f tg-launch-openai-cassandra.yaml up -d # Using Cassandra as the graph store
docker compose -f tg-launch-openai-neo4j.yaml up -d # Using Neo4j as the graph store
```
@ -458,4 +458,4 @@ docker compose -f tg-launch-<model-deployment>-<graph-store>.yaml down -v
> To confirm all Docker volumes have been removed, check that the following list is empty:
> ```
> docker volume ls
> ```
> ```

32
schema.ttl Normal file
View file

@ -0,0 +1,32 @@
@prefix ns1: <http://trustgraph.ai/e/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix schema: <https://schema.org/> .
@prefix skos: <http://www.w3.org/2004/02/skos/core#> .
schema:subjectOf rdfs:label "subject of" .
skos:definition rdfs:label "definition" .
rdf:type rdfs:label "type" .
schema:DigitalDocument rdfs:label "digital document" .
schema:Organization rdfs:label "organization" .
schema:PublicationEvent rdfs:label "publication event" .
schema:copyrightNotice rdfs:label "copyright notice" .
schema:copyrightHolder rdfs:label "copyright holder" .
schema:copyrightYear rdfs:label "copyright year" .
schema:license rdfs:label "license" .
schema:publication rdfs:label "publication" .
schema:startDate rdfs:label "start date" .
schema:endDate rdfs:label "end date" .
schema:publishedBy rdfs:label "published by" .
schema:datePublished rdfs:label "date published" .
schema:publication rdfs:label "publication" .
schema:datePublished rdfs:label "date published" .
schema:url rdfs:label "url" .
schema:identifier rdfs:label "identifier" .
schema:keywords rdfs:label "keyword" .
skos:definition rdfs:label "definition" .

View file

@ -7,6 +7,7 @@
import "patterns/triple-store-neo4j.jsonnet",
import "patterns/graph-rag.jsonnet",
import "patterns/llm-azure.jsonnet",
import "patterns/llm-azure-openai.jsonnet",
import "patterns/llm-bedrock.jsonnet",
import "patterns/llm-claude.jsonnet",
import "patterns/llm-cohere.jsonnet",

View file

@ -1,11 +1,13 @@
{
"azure": import "components/azure.jsonnet",
"azure-openai": import "components/azure-openai.jsonnet",
"bedrock": import "components/bedrock.jsonnet",
"claude": import "components/claude.jsonnet",
"cohere": import "components/cohere.jsonnet",
"document-rag": import "components/document-rag.jsonnet",
"embeddings-hf": import "components/embeddings-hf.jsonnet",
"embeddings-ollama": import "components/embeddings-ollama.jsonnet",
"googleaistudio": import "components/googleaistudio.jsonnet",
"grafana": import "components/grafana.jsonnet",
"graph-rag": import "components/graph-rag.jsonnet",
"triple-store-cassandra": import "components/cassandra.jsonnet",
@ -14,11 +16,10 @@
"ollama": import "components/ollama.jsonnet",
"openai": import "components/openai.jsonnet",
"override-recursive-chunker": import "components/chunker-recursive.jsonnet",
"prompt-template-definitions": import "components/null.jsonnet",
"prompt-template-document-query": import "components/null.jsonnet",
"prompt-template-kq-query": import "components/null.jsonnet",
"prompt-template-relationships": import "components/null.jsonnet",
"prompt-template-rows-template": import "components/null.jsonnet",
"prompt-template": import "components/prompt-template.jsonnet",
"prompt-overrides": import "components/prompt-overrides.jsonnet",
"pulsar": import "components/pulsar.jsonnet",
"pulsar-manager": import "components/pulsar-manager.jsonnet",
"trustgraph-base": import "components/trustgraph.jsonnet",
@ -27,6 +28,8 @@
"vertexai": import "components/vertexai.jsonnet",
"null": {},
"agent-manager-react": import "components/agent-manager-react.jsonnet",
// FIXME: Dupes
"cassandra": import "components/cassandra.jsonnet",
"neo4j": import "components/neo4j.jsonnet",

View file

@ -0,0 +1,60 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
local default_prompts = import "prompts/default-prompts.jsonnet";
{
tools:: [],
"agent-manager" +: {
create:: function(engine)
local container =
engine.container("agent-manager")
.with_image(images.trustgraph)
.with_command([
"agent-manager-react",
"-p",
url.pulsar,
"--tool-type",
] + [
tool.id + "=" + tool.type
for tool in $.tools
] + [
"--tool-description"
] + [
tool.id + "=" + tool.description
for tool in $.tools
] + [
"--tool-argument"
] + [
"%s=%s:%s:%s" % [
tool.id, arg.name, arg.type, arg.description
]
for tool in $.tools
for arg in tool.arguments
]
)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"agent-manager", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
},
} + default_prompts

View file

@ -0,0 +1,84 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
{
"azure-openai-model":: "GPT-3.5-Turbo",
"azure-openai-max-output-tokens":: 4192,
"azure-openai-temperature":: 0.0,
"text-completion" +: {
create:: function(engine)
local envSecrets = engine.envSecrets("azure-openai-credentials")
.with_env_var("AZURE_TOKEN", "azure-token");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
.with_command([
"text-completion-azure-openai",
"-p",
url.pulsar,
"-m",
$["azure-openai-model"],
"-x",
std.toString($["azure-openai-max-output-tokens"]),
"-t",
"%0.3f" % $["azure-openai-temperature"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-azure",
"-p",
url.pulsar,
"-x",
std.toString($["azure-openai-max-output-tokens"]),
"-t",
"%0.3f" % $["azure-openai-temperature"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
},
} + prompts

View file

@ -5,8 +5,6 @@ local prompts = import "prompts/mixtral.jsonnet";
{
"azure-token":: "${AZURE_TOKEN}",
"azure-endpoint":: "${AZURE_ENDPOINT}",
"azure-max-output-tokens":: 4096,
"azure-temperature":: 0.0,
@ -14,6 +12,10 @@ local prompts = import "prompts/mixtral.jsonnet";
create:: function(engine)
local envSecrets = engine.envSecrets("azure-credentials")
.with_env_var("AZURE_TOKEN", "azure-token")
.with_env_var("AZURE_ENDPOINT", "azure-endpoint");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -21,15 +23,32 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion-azure",
"-p",
url.pulsar,
"-k",
$["azure-token"],
"-e",
$["azure-endpoint"],
"-x",
std.toString($["azure-max-output-tokens"]),
"-t",
std.toString($["azure-temperature"]),
"%0.3f" % $["azure-temperature"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-azure",
"-p",
url.pulsar,
"-x",
std.toString($["azure-max-output-tokens"]),
"-t",
"%0.3f" % $["azure-temperature"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
@ -37,57 +56,25 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-azure",
"-p",
url.pulsar,
"-k",
$["azure-token"],
"-e",
$["azure-endpoint"],
"-x",
std.toString($["azure-max-output-tokens"]),
"-t",
std.toString($["azure-temperature"]),
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}

View file

@ -6,9 +6,6 @@ local chunker = import "chunker-recursive.jsonnet";
{
"aws-id-key":: "${AWS_ID_KEY}",
"aws-secret-key":: "${AWS_SECRET_KEY}",
"aws-region":: "us-west-2",
"bedrock-max-output-tokens":: 4096,
"bedrock-temperature":: 0.0,
"bedrock-model":: "mistral.mixtral-8x7b-instruct-v0:1",
@ -17,6 +14,11 @@ local chunker = import "chunker-recursive.jsonnet";
create:: function(engine)
local envSecrets = engine.envSecrets("bedrock-credentials")
.with_env_var("AWS_ID_KEY", "aws-id-key")
.with_env_var("AWS_SECRET", "aws-secret")
.with_env_var("AWS_REGION", "aws-region");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -24,58 +26,28 @@ local chunker = import "chunker-recursive.jsonnet";
"text-completion-bedrock",
"-p",
url.pulsar,
"-z",
$["aws-id-key"],
"-k",
$["aws-secret-key"],
"-r",
$["aws-region"],
"-x",
std.toString($["bedrock-max-output-tokens"]),
"-t",
std.toString($["bedrock-temperature"]),
"%0.3f" % $["bedrock-temperature"],
"-m",
$["bedrock-model"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-bedrock",
"-p",
url.pulsar,
"-z",
$["aws-id-key"],
"-k",
$["aws-secret-key"],
"-r",
$["aws-region"],
"-x",
std.toString($["bedrock-max-output-tokens"]),
"-t",
std.toString($["bedrock-temperature"]),
"%0.3f" % $["bedrock-temperature"],
"-m",
$["bedrock-model"],
"-i",
@ -83,24 +55,35 @@ local chunker = import "chunker-recursive.jsonnet";
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts + chunker

View file

@ -24,7 +24,7 @@ cassandra + {
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"stop-triples", [ container ]
"store-triples", [ container ]
);
local service =

View file

@ -5,7 +5,6 @@ local prompts = import "prompts/mixtral.jsonnet";
{
"claude-key":: "${CLAUDE_KEY}",
"claude-max-output-tokens":: 4096,
"claude-temperature":: 0.0,
@ -13,6 +12,9 @@ local prompts = import "prompts/mixtral.jsonnet";
create:: function(engine)
local envSecrets = engine.envSecrets("claude-credentials")
.with_env_var("CLAUDE_KEY_TOKEN", "claude-key");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -20,13 +22,32 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion-claude",
"-p",
url.pulsar,
"-k",
$["claude-key"],
"-x",
std.toString($["claude-max-output-tokens"]),
"-t",
std.toString($["claude-temperature"]),
"%0.3f" % $["claude-temperature"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-claude",
"-p",
url.pulsar,
"-x",
std.toString($["claude-max-output-tokens"]),
"-t",
"%0.3f" % $["claude-temperature"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
@ -34,57 +55,27 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-claude",
"-p",
url.pulsar,
"-k",
$["claude-key"],
"-x",
std.toString($["claude-max-output-tokens"]),
"-t",
std.toString($["claude-temperature"]),
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts

View file

@ -9,13 +9,15 @@ local prompts = import "prompts/mixtral.jsonnet";
"chunk-size":: 150,
"chunk-overlap":: 10,
"cohere-key":: "${COHERE_KEY}",
"cohere-temperature":: 0.0,
"text-completion" +: {
create:: function(engine)
local envSecrets = engine.envSecrets("cohere-credentials")
.with_env_var("COHERE_KEY", "cohere-key");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -23,44 +25,21 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion-cohere",
"-p",
url.pulsar,
"-k",
$["cohere-key"],
"-t",
std.toString($["cohere-temperature"]),
"%0.3f" % $["cohere-temperature"],
])
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-cohere",
"-p",
url.pulsar,
"-k",
$["cohere-key"],
"-t",
std.toString($["cohere-temperature"]),
"%0.3f" % $["cohere-temperature"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
@ -70,20 +49,30 @@ local prompts = import "prompts/mixtral.jsonnet";
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts

View file

@ -0,0 +1,86 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
{
"googleaistudio-max-output-tokens":: 4096,
"googleaistudio-temperature":: 0.0,
"googleaistudio-model":: "gemini-1.5-flash-002",
"text-completion" +: {
create:: function(engine)
local envSecrets = engine.envSecrets("bedrock-credentials")
.with_env_var("GOOGLE_AI_STUDIO_KEY", "googleaistudio-key");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
.with_command([
"text-completion-googleaistudio",
"-p",
url.pulsar,
"-x",
std.toString($["googleaistudio-max-output-tokens"]),
"-t",
"%0.3f" % $["googleaistudio-temperature"],
"-m",
$["googleaistudio-model"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-googleaistudio",
"-p",
url.pulsar,
"-x",
std.toString($["googleaistudio-max-output-tokens"]),
"-t",
"%0.3f" % $["googleaistudio-temperature"],
"-m",
$["googleaistudio-model"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8000, 8000, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8000, 8000, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
},
} + prompts

View file

@ -6,12 +6,14 @@ local prompts = import "prompts/slm.jsonnet";
{
"llamafile-model":: "LLaMA_CPP",
"llamafile-url":: "${LLAMAFILE_URL}",
"text-completion" +: {
create:: function(engine)
local envSecrets = engine.envSecrets("llamafile-credentials")
.with_env_var("LLAMAFILE_URL", "llamafile-url");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -21,27 +23,12 @@ local prompts = import "prompts/slm.jsonnet";
url.pulsar,
"-m",
$["llamafile-model"],
"-r",
$["llamafile-url"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
engine.resources([
containerSet,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
@ -50,26 +37,40 @@ local prompts = import "prompts/slm.jsonnet";
url.pulsar,
"-m",
$["llamafile-model"],
"-r",
$["llamafile-url"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8080, 8080, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts

View file

@ -1,17 +1,19 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/slm.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
{
"ollama-model":: "gemma2:9b",
"ollama-url":: "${OLLAMA_HOST}",
"text-completion" +: {
create:: function(engine)
local envSecrets = engine.envSecrets("ollama-credentials")
.with_env_var("OLLAMA_HOST", "ollama-host");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -21,32 +23,12 @@ local prompts = import "prompts/slm.jsonnet";
url.pulsar,
"-m",
$["ollama-model"],
"-r",
$["ollama-url"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
@ -55,31 +37,40 @@ local prompts = import "prompts/slm.jsonnet";
url.pulsar,
"-m",
$["ollama-model"],
"-r",
$["ollama-url"],
"-i",
"non-persistent://tg/request/text-completion-rag",
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8080, 8080, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts

View file

@ -5,7 +5,6 @@ local prompts = import "prompts/mixtral.jsonnet";
{
"openai-key":: "${OPENAI_KEY}",
"openai-max-output-tokens":: 4096,
"openai-temperature":: 0.0,
"openai-model":: "GPT-3.5-Turbo",
@ -14,6 +13,9 @@ local prompts = import "prompts/mixtral.jsonnet";
create:: function(engine)
local envSecrets = engine.envSecrets("openai-credentials")
.with_env_var("OPENAI_TOKEN", "openai-token");
local container =
engine.container("text-completion")
.with_image(images.trustgraph)
@ -21,50 +23,28 @@ local prompts = import "prompts/mixtral.jsonnet";
"text-completion-openai",
"-p",
url.pulsar,
"-k",
$["openai-key"],
"-x",
std.toString($["openai-max-output-tokens"]),
"-t",
std.toString($["openai-temperature"]),
"%0.3f" % $["openai-temperature"],
"-m",
$["openai-model"],
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
engine.resources([
containerSet,
service,
])
},
"text-completion-rag" +: {
create:: function(engine)
local container =
local containerRag =
engine.container("text-completion-rag")
.with_image(images.trustgraph)
.with_command([
"text-completion-openai",
"-p",
url.pulsar,
"-k",
$["openai-key"],
"-x",
std.toString($["openai-max-output-tokens"]),
"-t",
std.toString($["openai-temperature"]),
"%0.3f" % $["openai-temperature"],
"-m",
$["openai-model"],
"-i",
@ -72,24 +52,35 @@ local prompts = import "prompts/mixtral.jsonnet";
"-o",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_env_var_secrets(envSecrets)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"text-completion-rag", [ container ]
"text-completion", [ container ]
);
local containerSetRag = engine.containers(
"text-completion-rag", [ containerRag ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
local serviceRag =
engine.internalService(containerSetRag)
.with_port(8080, 8080, "metrics");
engine.resources([
envSecrets,
containerSet,
containerSetRag,
service,
serviceRag,
])
}
},
} + prompts

View file

@ -1,81 +0,0 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
{
"prompt" +: {
create:: function(engine)
local container =
engine.container("prompt")
.with_image(images.trustgraph)
.with_command([
"prompt-generic",
"-p",
url.pulsar,
"--text-completion-request-queue",
"non-persistent://tg/request/text-completion",
"--text-completion-response-queue",
"non-persistent://tg/response/text-completion-response",
])
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"prompt", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
engine.resources([
containerSet,
service,
])
},
"prompt-rag" +: {
create:: function(engine)
local container =
engine.container("prompt-rag")
.with_image(images.trustgraph)
.with_command([
"prompt-generic",
"-p",
url.pulsar,
"-i",
"non-persistent://tg/request/prompt-rag",
"-o",
"non-persistent://tg/response/prompt-rag-response",
"--text-completion-request-queue",
"non-persistent://tg/request/text-completion-rag",
"--text-completion-response-queue",
"non-persistent://tg/response/text-completion-rag-response",
])
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
local containerSet = engine.containers(
"prompt-rag", [ container ]
);
local service =
engine.internalService(containerSet)
.with_port(8080, 8080, "metrics");
engine.resources([
containerSet,
service,
])
},
}

View file

@ -0,0 +1,28 @@
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
local prompts = import "prompts/mixtral.jsonnet";
local default_prompts = import "prompts/default-prompts.jsonnet";
{
with:: function(key, value)
if (key == "system-template") then
self + {
prompts +:: {
"system-template": value,
}
}
else
self + {
prompts +:: {
templates +:: {
[key] +:: {
prompt: value
}
}
}
},
} + default_prompts

View file

@ -6,6 +6,38 @@ local default_prompts = import "prompts/default-prompts.jsonnet";
{
prompts:: default_prompts,
local prompt_template_args = [ "--prompt" ] + [
p.key + "=" + p.value.prompt,
for p in std.objectKeysValuesAll($.prompts.templates)
],
local prompt_response_type_args = [ "--prompt-response-type" ] + [
p.key + "=" + p.value["response-type"],
for p in std.objectKeysValuesAll($.prompts.templates)
if std.objectHas(p.value, "response-type")
],
local prompt_schema_args = [ "--prompt-schema" ] + [
(
p.key + "=" +
std.manifestJsonMinified(p.value["schema"])
)
for p in std.objectKeysValuesAll($.prompts.templates)
if std.objectHas(p.value, "schema")
],
local prompt_term_args = [ "--prompt-term" ] + [
p.key + "=" + t.key + ":" + t.value
for p in std.objectKeysValuesAll($.prompts.templates)
if std.objectHas(p.value, "terms")
for t in std.objectKeysValuesAll(p.value.terms)
],
local prompt_args = prompt_template_args + prompt_response_type_args +
prompt_schema_args + prompt_term_args,
"prompt" +: {
create:: function(engine)
@ -17,23 +49,17 @@ local default_prompts = import "prompts/default-prompts.jsonnet";
"prompt-template",
"-p",
url.pulsar,
"--text-completion-request-queue",
"non-persistent://tg/request/text-completion",
"--text-completion-response-queue",
"non-persistent://tg/response/text-completion-response",
"--definition-template",
$["prompt-definition-template"],
"--relationship-template",
$["prompt-relationship-template"],
"--topic-template",
$["prompt-topic-template"],
"--knowledge-query-template",
$["prompt-knowledge-query-template"],
"--document-query-template",
$["prompt-document-query-template"],
"--rows-template",
$["prompt-rows-template"],
])
"--system-prompt",
$["prompts"]["system-template"],
] + prompt_args
)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");
@ -71,19 +97,12 @@ local default_prompts = import "prompts/default-prompts.jsonnet";
"non-persistent://tg/request/text-completion-rag",
"--text-completion-response-queue",
"non-persistent://tg/response/text-completion-rag-response",
"--definition-template",
$["prompt-definition-template"],
"--relationship-template",
$["prompt-relationship-template"],
"--topic-template",
$["prompt-topic-template"],
"--knowledge-query-template",
$["prompt-knowledge-query-template"],
"--document-query-template",
$["prompt-document-query-template"],
"--rows-template",
$["prompt-rows-template"],
])
"--system-prompt",
$["prompts"]["system-template"],
] + prompt_args
)
.with_limits("0.5", "128M")
.with_reservations("0.1", "128M");

View file

@ -37,7 +37,7 @@ local prompts = import "prompts/mixtral.jsonnet";
"-x",
std.toString($["vertexai-max-output-tokens"]),
"-t",
std.toString($["vertexai-temperature"]),
"%0.3f" % $["vertexai-temperature"],
"-m",
$["vertexai-model"],
])
@ -87,7 +87,7 @@ local prompts = import "prompts/mixtral.jsonnet";
"-x",
std.toString($["vertexai-max-output-tokens"]),
"-t",
std.toString($["vertexai-temperature"]),
"%0.3f" % $["vertexai-temperature"],
"-m",
$["vertexai-model"],
"-i",

View file

@ -18,12 +18,15 @@
reservations: {},
ports: [],
volumes: [],
environment: {},
with_image:: function(x) self + { image: x },
with_command:: function(x) self + { command: x },
with_environment:: function(x) self + { environment: x },
with_environment:: function(x) self + {
environment: super.environment + x,
},
with_limits:: function(c, m) self + { limits: { cpus: c, memory: m } },
@ -45,6 +48,16 @@
]
},
with_env_var_secrets::
function(vars)
std.foldl(
function(obj, x) obj.with_environment(
{ [x]: "${" + x + "}" }
),
vars.variables,
self
),
add:: function() {
services +: {
[container.name]: {
@ -62,7 +75,7 @@
{ command: container.command }
else {}) +
(if std.objectHas(container, "environment") then
(if ! std.isEmpty(container.environment) then
{ environment: container.environment }
else {}) +
@ -170,6 +183,27 @@
},
envSecrets:: function(name)
{
local volume = self,
name: name,
volid:: name,
variables:: [],
with_env_var::
function(name, key) self + {
variables: super.variables + [name],
},
add:: function() {
}
},
containers:: function(name, containers)
{

View file

@ -10,12 +10,20 @@
reservations: {},
ports: [],
volumes: [],
environment: [],
with_image:: function(x) self + { image: x },
with_command:: function(x) self + { command: x },
with_environment:: function(x) self + { environment: x },
with_environment:: function(x) self + {
environment: super.environment + [
{
name: v.key, value: v.value
}
for v in std.objectKeysValues(x)
],
},
with_limits:: function(c, m) self + { limits: { cpu: c, memory: m } },
@ -37,6 +45,24 @@
]
},
with_env_var_secrets::
function(vars)
std.foldl(
function(obj, x) obj + {
environment: super.environment + [{
name: x,
valueFrom: {
secretKeyRef: {
name: vars.name,
key: vars.keyMap[x],
}
}
}]
},
vars.variables,
self
),
add:: function() [
{
@ -97,16 +123,11 @@
(if std.objectHas(container, "command") then
{ command: container.command }
else {}) +
(if std.objectHas(container, "environment") then
{ env: [ {
name: e.key, value: e.value
}
for e in
std.objectKeysValues(
container.environment
)
]
}
(if ! std.isEmpty(container.environment) then
{
env: container.environment,
}
else {}) +
(if std.length(container.volumes) > 0 then
@ -283,6 +304,34 @@
},
envSecrets:: function(name)
{
local volume = self,
name: name,
variables: [],
keyMap: {},
with_size:: function(size) self + { size: size },
add:: function() [
],
volRef:: function() {
name: volume.name,
secret: { secretName: volume.name },
},
with_env_var::
function(name, key) self + {
variables: super.variables + [name],
keyMap: super.keyMap + { [name]: key },
},
},
containers:: function(name, containers)
{

140
templates/generate Executable file
View file

@ -0,0 +1,140 @@
#!/usr/bin/env python3
import _jsonnet as j
import json
import yaml
import logging
import os
import sys
import zipfile
logger = logging.getLogger("generate")
logging.basicConfig(level=logging.INFO, format='%(message)s')
private_json = "Put your GCP private.json here"
class Generator:
def __init__(self, config, base="./templates/", version="0.0.0"):
self.jsonnet_base = base
self.config = config
self.version = f"\"{version}\"".encode("utf-8")
def process(self, config):
res = j.evaluate_snippet("config", config, import_callback=self.load)
return json.loads(res)
def load(self, dir, filename):
logger.debug("Request jsonnet: %s %s", dir, filename)
if filename == "config.json" and dir == "":
path = os.path.join(".", dir, filename)
return str(path), self.config
if filename == "version.jsonnet" and dir == "./templates/values/":
path = os.path.join(".", dir, filename)
return str(path), self.version
if dir:
candidates = [
os.path.join(".", dir, filename),
os.path.join(".", filename)
]
else:
candidates = [
os.path.join(".", filename)
]
try:
if filename == "vertexai/private.json":
return candidates[0], private_json.encode("utf-8")
for c in candidates:
logger.debug("Try: %s", c)
if os.path.isfile(c):
with open(c, "rb") as f:
logger.debug("Loading: %s", c)
return str(c), f.read()
raise RuntimeError(
f"Could not load file={filename} dir={dir}"
)
except:
path = os.path.join(self.jsonnet_base, filename)
logger.debug("Try: %s", path)
with open(path, "rb") as f:
logger.debug("Loaded: %s", path)
return str(path), f.read()
def main():
if len(sys.argv) != 3:
print()
print("Usage:")
print(" generate <outfile> <version> < input.json")
print()
raise RuntimeError("Arg error")
outfile = sys.argv[1]
version = sys.argv[2]
cfg = sys.stdin.read()
cfg = json.loads(cfg)
logger.info(f"Outputting to {outfile}...")
with zipfile.ZipFile(outfile, mode='w') as out:
def output(name, content):
logger.info(f"Adding {name}...")
out.writestr(name, content)
fname = "tg-launch.yaml"
platform = "docker-compose"
with open(f"./templates/config-to-{platform}.jsonnet", "r") as f:
wrapper = f.read()
gen = Generator(json.dumps(cfg).encode("utf-8"), version=version)
processed = gen.process(wrapper)
y = yaml.dump(processed)
output(fname, y)
# Placeholder for the private.json file. Won't put actual credentials
# here.
output("docker-compose/vertexai/private.json", private_json)
# Grafana config
with open("grafana/dashboards/dashboard.json") as f:
output(
"docker-compose/grafana/dashboards/dashboard.json", f.read()
)
with open("grafana/provisioning/dashboard.yml") as f:
output(
"docker-compose/grafana/provisioning/dashboard.yml", f.read()
)
with open("grafana/provisioning/datasource.yml") as f:
output(
"docker-compose/grafana/provisioning/datasource.yml", f.read()
)
# Prometheus config
with open("prometheus/prometheus.yml") as f:
output("docker-compose/prometheus/prometheus.yml", f.read())
main()

View file

@ -122,8 +122,8 @@ def generate_all(output, version):
"docker-compose", "minikube-k8s", "gcp-k8s"
]:
for model in [
"azure", "bedrock", "claude", "cohere", "llamafile", "ollama",
"openai", "vertexai"
"azure", "azure-openai", "bedrock", "claude", "cohere",
"googleaistudio", "llamafile", "ollama", "openai", "vertexai",
]:
for graph in [ "cassandra", "neo4j" ]:

View file

@ -0,0 +1,32 @@
{
pattern: {
name: "azure-openai",
icon: "🤖💬",
title: "Add Azure OpenAI LLM endpoint for text completion",
description: "This pattern integrates an Azure OpenAI LLM endpoint hosted in the Azure cloud for text completion operations. You need an Azure subscription to be able to use this service.",
requires: ["pulsar", "trustgraph"],
features: ["llm"],
args: [
{
name: "azure-openai-max-output-tokens",
label: "Maximum output tokens",
type: "integer",
description: "Limit on number tokens to generate",
default: 4096,
required: true,
},
{
name: "azure-openai-temperature",
label: "Temperature",
type: "slider",
description: "Controlling predictability / creativity balance",
min: 0,
max: 1,
step: 0.05,
default: 0.5,
},
],
category: [ "llm" ],
},
module: "components/azure.jsonnet",
}

View file

@ -0,0 +1,32 @@
{
pattern: {
name: "googleaistudio",
icon: "🤖💬",
title: "Add GoogleAIStudio for text completion",
description: "This pattern integrates a GoogleAIStudio LLM service for text completion operations. You need a GoogleAISTudio API key to be able to use this service.",
requires: ["pulsar", "trustgraph"],
features: ["llm"],
args: [
{
name: "googleaistudio-max-output-tokens",
label: "Maximum output tokens",
type: "integer",
description: "Limit on number tokens to generate",
default: 4096,
required: true,
},
{
name: "googleaistudio-temperature",
label: "Temperature",
type: "slider",
description: "Controlling predictability / creativity balance",
min: 0,
max: 1,
step: 0.05,
default: 0.5,
},
],
category: [ "llm" ],
},
module: "components/googleaistudio.jsonnet",
}

View file

@ -1,18 +1,42 @@
// For Cohere. Not currently overriding prompts
{
local prompts = import "default-prompts.jsonnet";
// "prompt-definition-template": "PROMPT GOES HERE",
prompts + {
// "prompt-relationship-template":: "PROMPT GOES HERE",
// "system-template":: "PROMPT GOES HERE.",
// "prompt-topic-template":: "PROMPT GOES HERE",
"templates" +:: {
// "prompt-knowledge-query-template":: "PROMPT GOES HERE",
"question" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-document-query-template":: "PROMPT GOES HERE",
"extract-definitions" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-rows-template":: "PROMPT GOES HERE",
"extract-relationships" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-topics" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-rows" +:: {
// "prompt": "PROMPT GOES HERE",
},
"kg-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
"document-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
}
}

View file

@ -4,16 +4,111 @@
{
"prompt-definition-template":: "<instructions>\nStudy the following text and derive definitions for any discovered entities.\nDo not provide definitions for entities whose definitions are incomplete\nor unknown.\nOutput relationships in JSON format as an arary of objects with fields:\n- entity: the name of the entity\n- definition: English text which defines the entity\n</instructions>\n\n<text>\n{text}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not use special characters in the abstract text. The\nabstract will be written as plain text. Do not add markdown formatting\nor headers or prefixes. Do not include null or unknown definitions.\n</requirements>",
"system-template":: "You are a helpful assistant.",
"prompt-relationship-template":: "<instructions>\nStudy the following text and derive entity relationships. For each\nrelationship, derive the subject, predicate and object of the relationship.\nOutput relationships in JSON format as an arary of objects with fields:\n- subject: the subject of the relationship\n- predicate: the predicate\n- object: the object of the relationship\n- object-entity: false if the object is a simple data type: name, value or date. true if it is an entity.\n</instructions>\n\n<text>\n{text}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not use special characters in the abstract text. The\nabstract must be written as plain text. Do not add markdown formatting\nor headers or prefixes.\n</requirements>",
"templates":: {
"prompt-topic-template":: "You are a helpful assistant that performs information extraction tasks for a provided text.\nRead the provided text. You will identify topics and their definitions in JSON.\n\nReading Instructions:\n- Ignore document formatting in the provided text.\n- Study the provided text carefully.\n\nHere is the text:\n{text}\n\nResponse Instructions: \n- Do not respond with special characters.\n- Return only topics that are concepts and unique to the provided text.\n- Respond only with well-formed JSON.\n- The JSON response shall be an array of objects with keys \"topic\" and \"definition\". \n- The JSON response shall use the following structure:\n\n```json\n[{{\"topic\": string, \"definition\": string}}]\n```\n\n- Do not write any additional text or explanations.",
"question":: {
"prompt": "{{question}}",
},
"prompt-knowledge-query-template":: "Study the following set of knowledge statements. The statements are written in Cypher format that has been extracted from a knowledge graph. Use only the provided set of knowledge statements in your response. Do not speculate if the answer is not found in the provided set of knowledge statements.\n\nHere's the knowledge statements:\n{graph}\n\nUse only the provided knowledge statements to respond to the following:\n{query}\n",
"extract-definitions":: {
"prompt": "<instructions>\nStudy the following text and derive definitions for any discovered entities.\nDo not provide definitions for entities whose definitions are incomplete\nor unknown.\nOutput relationships in JSON format as an arary of objects with fields:\n- entity: the name of the entity\n- definition: English text which defines the entity\n</instructions>\n\n<text>\n{{text}}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not use special characters in the abstract text. The\nabstract will be written as plain text. Do not add markdown formatting\nor headers or prefixes. Do not include null or unknown definitions.\n</requirements>",
"response-type": "json",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entity": {
"type": "string"
},
"definition": {
"type": "string"
}
},
"required": [
"entity",
"definition"
]
}
}
},
"prompt-document-query-template":: "Study the following context. Use only the information provided in the context in your response. Do not speculate if the answer is not found in the provided set of knowledge statements.\n\nHere is the context:\n{documents}\n\nUse only the provided knowledge statements to respond to the following:\n{query}\n",
"extract-relationships":: {
"prompt": "<instructions>\nStudy the following text and derive entity relationships. For each\nrelationship, derive the subject, predicate and object of the relationship.\nOutput relationships in JSON format as an arary of objects with fields:\n- subject: the subject of the relationship\n- predicate: the predicate\n- object: the object of the relationship\n- object-entity: false if the object is a simple data type: name, value or date. true if it is an entity.\n</instructions>\n\n<text>\n{{text}}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not use special characters in the abstract text. The\nabstract must be written as plain text. Do not add markdown formatting\nor headers or prefixes.\n</requirements>",
"response-type": "json",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subject": {
"type": "string"
},
"predicate": {
"type": "string"
},
"object": {
"type": "string"
},
"object-entity": {
"type": "boolean"
},
},
"required": [
"subject",
"predicate",
"object",
"object-entity"
]
}
}
},
"prompt-rows-template":: "<instructions>\nStudy the following text and derive objects which match the schema provided.\n\nYou must output an array of JSON objects for each object you discover\nwhich matches the schema. For each object, output a JSON object whose fields\ncarry the name field specified in the schema.\n</instructions>\n\n<schema>\n{schema}\n</schema>\n\n<text>\n{text}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not add markdown formatting or headers or prefixes.\n</requirements>",
"extract-topics":: {
"prompt": "You are a helpful assistant that performs information extraction tasks for a provided text.\nRead the provided text. You will identify topics and their definitions in JSON.\n\nReading Instructions:\n- Ignore document formatting in the provided text.\n- Study the provided text carefully.\n\nHere is the text:\n{{text}}\n\nResponse Instructions: \n- Do not respond with special characters.\n- Return only topics that are concepts and unique to the provided text.\n- Respond only with well-formed JSON.\n- The JSON response shall be an array of objects with keys \"topic\" and \"definition\". \n- The JSON response shall use the following structure:\n\n```json\n[{\"topic\": string, \"definition\": string}]\n```\n\n- Do not write any additional text or explanations.",
"response-type": "json",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"topic": {
"type": "string"
},
"definition": {
"type": "string"
}
},
"required": [
"topic",
"definition"
]
}
}
},
"extract-rows":: {
"prompt": "<instructions>\nStudy the following text and derive objects which match the schema provided.\n\nYou must output an array of JSON objects for each object you discover\nwhich matches the schema. For each object, output a JSON object whose fields\ncarry the name field specified in the schema.\n</instructions>\n\n<schema>\n{{schema}}\n</schema>\n\n<text>\n{{text}}\n</text>\n\n<requirements>\nYou will respond only with raw JSON format data. Do not provide\nexplanations. Do not add markdown formatting or headers or prefixes.\n</requirements>",
"response-type": "json",
},
"kg-prompt":: {
"prompt": "Study the following set of knowledge statements. The statements are written in Cypher format that has been extracted from a knowledge graph. Use only the provided set of knowledge statements in your response. Do not speculate if the answer is not found in the provided set of knowledge statements.\n\nHere's the knowledge statements:\n{% for edge in knowledge %}({{edge.s}})-[{{edge.p}}]->({{edge.o}})\n{%endfor%}\n\nUse only the provided knowledge statements to respond to the following:\n{{query}}\n",
"response-type": "text",
},
"document-prompt":: {
"prompt": "Study the following context. Use only the information provided in the context in your response. Do not speculate if the answer is not found in the provided set of knowledge statements.\n\nHere is the context:\n{{documents}}\n\nUse only the provided knowledge statements to respond to the following:\n{{query}}\n",
"response-type": "text",
},
"agent-react":: {
"prompt": "Answer the following questions as best you can. You have\naccess to the following functions:\n\n{% for tool in tools %}{\n \"function\": \"{{ tool.name }}\",\n \"description\": \"{{ tool.description }}\",\n \"arguments\": [\n{% for arg in tool.arguments %} {\n \"name\": \"{{ arg.name }}\",\n \"type\": \"{{ arg.type }}\",\n \"description\": \"{{ arg.description }}\",\n }\n{% endfor %}\n ]\n}\n{% endfor %}\n\nYou can either choose to call a function to get more information, or\nreturn a final answer.\n \nTo call a function, respond with a JSON object of the following format:\n\n{\n \"thought\": \"your thought about what to do\",\n \"action\": \"the action to take, should be one of [{{tool_names}}]\",\n \"arguments\": {\n \"argument1\": \"argument_value\",\n \"argument2\": \"argument_value\"\n }\n}\n\nTo provide a final answer, response a JSON object of the following format:\n\n{\n \"thought\": \"I now know the final answer\",\n \"final-answer\": \"the final answer to the original input question\"\n}\n\nPrevious steps are included in the input. Each step has the following\nformat in your output:\n\n{\n \"thought\": \"your thought about what to do\",\n \"action\": \"the action taken\",\n \"arguments\": {\n \"argument1\": action argument,\n \"argument2\": action argument2\n },\n \"observation\": \"the result of the action\",\n}\n\nRespond by describing either one single thought/action/arguments or\nthe final-answer. Pause after providing one action or final-answer.\n\n{% if context %}Additional context has been provided:\n{{context}}{% endif %}\n\nQuestion: {{question}}\n\nInput:\n \n{% for h in history %}\n{\n \"action\": \"{{h.action}}\",\n \"arguments\": [\n{% for k, v in h.arguments.items() %} {\n \"{{k}}\": \"{{v}}\",\n{%endfor%} }\n ],\n \"observation\": \"{{h.observation}}\"\n}\n{% endfor %}",
"response-type": "json"
}
}
}
}

View file

@ -1,17 +1,42 @@
// For VertexAI Gemini. Not currently overriding prompts
{
// "prompt-definition-template": "PROMPT GOES HERE",
local prompts = import "default-prompts.jsonnet";
// "prompt-relationship-template":: "PROMPT GOES HERE",
prompts + {
// "prompt-topic-template":: "PROMPT GOES HERE",
// "system-template":: "PROMPT GOES HERE.",
// "prompt-knowledge-query-template":: "PROMPT GOES HERE",
"templates" +:: {
// "prompt-document-query-template":: "PROMPT GOES HERE",
"question" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-rows-template":: "PROMPT GOES HERE",
"extract-definitions" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-relationships" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-topics" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-rows" +:: {
// "prompt": "PROMPT GOES HERE",
},
"kg-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
"document-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
}
}

View file

@ -1,18 +1,42 @@
// For Mixtral. Not currently overriding prompts
{
local prompts = import "default-prompts.jsonnet";
// "prompt-definition-template": "PROMPT GOES HERE",
prompts + {
// "prompt-relationship-template":: "PROMPT GOES HERE",
// "system-template":: "PROMPT GOES HERE.",
// "prompt-topic-template":: "PROMPT GOES HERE",
"templates" +:: {
// "prompt-knowledge-query-template":: "PROMPT GOES HERE",
"question" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-document-query-template":: "PROMPT GOES HERE",
"extract-definitions" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-rows-template":: "PROMPT GOES HERE",
"extract-relationships" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-topics" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-rows" +:: {
// "prompt": "PROMPT GOES HERE",
},
"kg-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
"document-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
}
}

View file

@ -1,23 +1,42 @@
// For OpenAI LLMs. Not currently overriding prompts
// For OpenAI LLMs
local prompts = import "default-prompts.jsonnet";
local base = import "base/base.jsonnet";
local images = import "values/images.jsonnet";
local url = import "values/url.jsonnet";
prompts + {
{
// "system-template":: "PROMPT GOES HERE.",
// "prompt-definition-template": "PROMPT GOES HERE",
"templates" +:: {
// "prompt-relationship-template":: "PROMPT GOES HERE",
"question" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-topic-template":: "PROMPT GOES HERE",
"extract-definitions" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-knowledge-query-template":: "PROMPT GOES HERE",
"extract-relationships" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-document-query-template":: "PROMPT GOES HERE",
"extract-topics" +:: {
// "prompt": "PROMPT GOES HERE",
},
// "prompt-rows-template":: "PROMPT GOES HERE",
"extract-rows" +:: {
// "prompt": "PROMPT GOES HERE",
},
"kg-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
"document-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
}
}

View file

@ -1,7 +1,44 @@
// For SLM. Not currently overriding prompts
// For basic SLMs, use prompt-generic
local prompts = import "default-prompts.jsonnet";
prompts + {
// "system-template":: "PROMPT GOES HERE.",
"templates" +:: {
"question" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-definitions" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-relationships" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-topics" +:: {
// "prompt": "PROMPT GOES HERE",
},
"extract-rows" +:: {
// "prompt": "PROMPT GOES HERE",
},
"kg-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
"document-prompt" +:: {
// "prompt": "PROMPT GOES HERE",
},
}
}
local prompts = import "components/prompt-generic.jsonnet";
prompts

View file

@ -13,10 +13,10 @@ local images = import "values/images.jsonnet";
engine.container("cassandra")
.with_image(images.cassandra)
.with_environment({
JVM_OPTS: "-Xms256M -Xmx256M",
JVM_OPTS: "-Xms300M -Xmx300M",
})
.with_limits("1.0", "800M")
.with_reservations("0.5", "800M")
.with_limits("1.0", "1000M")
.with_reservations("0.5", "1000M")
.with_port(9042, 9042, "cassandra")
.with_volume_mount(vol, "/var/lib/cassandra");

View file

@ -37,7 +37,7 @@ local images = import "values/images.jsonnet";
local service =
engine.service(containerSet)
.with_port(2379, 2379, 30379, "api");
.with_port(2379, 2379, "api");
engine.resources([
vol,
@ -117,7 +117,7 @@ local images = import "values/images.jsonnet";
local service =
engine.service(containerSet)
.with_port(9091, 9091, "api")
.with_port(19530, 19530, "api2);
.with_port(19530, 19530, "api2");
engine.resources([
vol,

View file

@ -12,8 +12,8 @@ local images = import "values/images.jsonnet";
local container =
engine.container("qdrant")
.with_image(images.qdrant)
.with_limits("1.0", "256M")
.with_reservations("0.5", "256M")
.with_limits("1.0", "1024M")
.with_reservations("0.5", "1024M")
.with_port(6333, 6333, "api")
.with_port(6334, 6334, "api2")
.with_volume_mount(vol, "/qdrant/storage");

View file

@ -3,9 +3,7 @@ local components = import "components.jsonnet";
local apply = function(p, components)
local component = components[p.name];
(component + {
local base = {
with:: function(k, v) self + {
[k]:: v
@ -18,7 +16,11 @@ local apply = function(p, components)
self
),
}).with_params(p.parameters);
};
local component = base + components[p.name];
component.with_params(p.parameters);
local decode = function(config)
local add = function(state, c) state + apply(c, components);

27
tests/README.prompts Normal file
View file

@ -0,0 +1,27 @@
test-prompt-... is tested with this prompt set...
prompt-template \
-p pulsar://localhost:6650 \
--system-prompt 'You are a {{attitude}}, you are called {{name}}' \
--global-term \
'name=Craig' \
'attitude=LOUD, SHOUTY ANNOYING BOT' \
--prompt \
'question={{question}}' \
'french-question={{question}}' \
"analyze=Find the name and age in this text, and output a JSON structure containing just the name and age fields: {{description}}. Don't add markup, just output the raw JSON object." \
"graph-query=Study the following knowledge graph, and then answer the question.\\n\nGraph:\\n{% for edge in knowledge %}({{edge.0}})-[{{edge.1}}]->({{edge.2}})\\n{%endfor%}\\nQuestion:\\n{{question}}" \
"extract-definition=Analyse the text provided, and then return a list of terms and definitions. The output should be a JSON array, each item in the array is an object with fields 'term' and 'definition'.Don't add markup, just output the raw JSON object. Here is the text:\\n{{text}}" \
--prompt-response-type \
'question=text' \
'analyze=json' \
'graph-query=text' \
'extract-definition=json' \
--prompt-term \
'question=name:Bonny' \
'french-question=attitude:French-speaking bot' \
--prompt-schema \
'analyze={ "type" : "object", "properties" : { "age": { "type" : "number" }, "name": { "type" : "string" } } }' \
'extract-definition={ "type": "array", "items": { "type": "object", "properties": { "term": { "type": "string" }, "definition": { "type": "string" } }, "required": [ "term", "definition" ] } }'

44
tests/test-agent Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
import json
import textwrap
from trustgraph.clients.agent_client import AgentClient
def wrap(text, width=75):
if text is None: text = "n/a"
out = textwrap.wrap(
text, width=width
)
return "\n".join(out)
def output(text, prefix="> ", width=78):
out = textwrap.indent(
text, prefix=prefix
)
print(out)
p = AgentClient(pulsar_host="pulsar://localhost:6650")
q = "How many cats does Mark have? Calculate that number raised to 0.4 power. Is that number lower than the numeric part of the mission identifier of the Space Shuttle Challenger on its last mission? If so, give me an apple pie recipe, otherwise return a poem about cheese."
output(wrap(q), "\U00002753 ")
print()
def think(x):
output(wrap(x), "\U0001f914 ")
print()
def observe(x):
output(wrap(x), "\U0001f4a1 ")
print()
resp = p.request(
question=q, think=think, observe=observe,
)
output(resp, "\U0001f4ac ")
print()

View file

@ -7,7 +7,13 @@ p = PromptClient(pulsar_host="pulsar://localhost:6650")
chunk = """I noticed a cat in my garden. It is a four-legged animal
which is a mammal and can be tame or wild. I wonder if it will be friends
with me. I think the cat's name is Fred and it has 4 legs"""
with me. I think the cat's name is Fred and it has 4 legs.
A cat is a small mammal.
A grapefruit is a citrus fruit.
"""
resp = p.request_definitions(
chunk=chunk,

19
tests/test-lang-topics Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import pulsar
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
chunk = """I noticed a cat in my garden. It is a four-legged animal
which is a mammal and can be tame or wild. I wonder if it will be friends
with me. I think the cat's name is Fred and it has 4 legs"""
resp = p.request_topics(
chunk=chunk,
)
for d in resp:
print(d.topic)
print(" ", d.definition)

View file

@ -5,9 +5,10 @@ from trustgraph.clients.llm_client import LlmClient
llm = LlmClient(pulsar_host="pulsar://localhost:6650")
system = "You are a lovely assistant."
prompt="Write a funny limerick about a llama"
resp = llm.request(prompt)
resp = llm.request(system, prompt)
print(resp)

18
tests/test-prompt-analyze Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import json
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
description = """Fred is a 4-legged cat who is 12 years old"""
resp = p.request(
id="analyze",
terms = {
"description": description,
}
)
print(json.dumps(resp, indent=4))

46
tests/test-prompt-extraction Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env python3
import json
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
chunk="""
The Space Shuttle was a reusable spacecraft that transported astronauts and cargo to and from Earth's orbit. It was designed to launch like a rocket, maneuver in orbit like a spacecraft, and land like an airplane. The Space Shuttle was NASA's space transportation system and was used for many purposes, including:
Carrying astronauts
The Space Shuttle could carry up to seven astronauts at a time.
Launching, recovering, and repairing satellites
The Space Shuttle could launch satellites into orbit, recover them, and repair them.
Building the International Space Station
The Space Shuttle carried large parts into space to build the International Space Station.
Conducting research
Astronauts conducted experiments in the Space Shuttle, which was like a science lab in space.
The Space Shuttle was retired in 2011 after the Columbia accident in 2003. The Columbia Accident Investigation Board report found that the Space Shuttle was unsafe and expensive to make safe.
Here are some other facts about the Space Shuttle:
The Space Shuttle was 184 ft tall and had a diameter of 29 ft.
The Space Shuttle had a mass of 4,480,000 lb.
The Space Shuttle's first flight was on April 12, 1981.
The Space Shuttle's last mission was in 2011.
"""
q = "Tell me some facts in the knowledge graph"
resp = p.request(
id="extract-definition",
terms = {
"text": chunk,
}
)
print(resp)
for fact in resp:
print(fact["term"], "::")
print(fact["definition"])
print()

View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import pulsar
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
question = """What is the square root of 16?"""
resp = p.request(
id="french-question",
terms = {
"question": question
}
)
print(resp)

44
tests/test-prompt-knowledge Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
import json
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
knowledge = [
("accident", "evoked", "a wide range of deeply felt public responses"),
("Space Shuttle concept", "had", "genesis"),
("Commission", "had", "a mandate to develop recommendations for corrective or other action based upon the Commission's findings and determinations"),
("Commission", "established", "teams of persons"),
("Space Shuttle Challenger", "http://www.w3.org/2004/02/skos/core#definition", "A space shuttle that was destroyed in an accident during mission 51-L."),
("The mid fuselage", "contains", "the payload bay"),
("Volume I", "contains", "Chapter IX"),
("accident", "resulted in", "firm national resolve that those men and women be forever enshrined in the annals of American heroes"),
("Volume I", "contains", "Chapter VII"),
("Volume I", "contains", "Chapter II"),
("Volume I", "contains", "Chapter V"),
("Commission", "believes", "its investigation and report have been responsive to the request of the President and hopes that they will serve the best interests of the nation in restoring the United States space program to its preeminent position in the world"),
("Commission", "construe", "mandate"),
("accident", "became", "a milestone on the way to achieving the full potential that space offers to mankind"),
("Volume I", "contains", "The Commission"),
("Commission", "http://www.w3.org/2004/02/skos/core#definition", "A group established to investigate the space shuttle accident"),
("Volume I", "contains", "Appendix D"),
("Commission", "had", "a mandate to review the circumstances surrounding the accident to establish the probable cause or causes of the accident"),
("Volume I", "contains", "Recommendations")
]
q = "Tell me some facts in the knowledge graph"
resp = p.request(
id="graph-query",
terms = {
"name": "Jayney",
"knowledge": knowledge,
"question": q
}
)
print(resp)

18
tests/test-prompt-question Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
import pulsar
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
question = """What is the square root of 16?"""
resp = p.request(
id="question",
terms = {
"question": question
}
)
print(resp)

View file

@ -0,0 +1,19 @@
#!/usr/bin/env python3
import pulsar
from trustgraph.clients.prompt_client import PromptClient
p = PromptClient(pulsar_host="pulsar://localhost:6650")
question = """What is the square root of 16?"""
resp = p.request(
id="question",
terms = {
"question": question,
"attitude": "Spanish-speaking bot"
}
)
print(resp)

View file

@ -39,8 +39,9 @@ class BaseProcessor:
def __del__(self):
if self.client:
self.client.close()
if hasattr(self, "client"):
if self.client:
self.client.close()
@staticmethod
def add_args(parser):

View file

@ -0,0 +1,64 @@
import _pulsar
from .. schema import AgentRequest, AgentResponse
from .. schema import agent_request_queue
from .. schema import agent_response_queue
from . base import BaseClient
# Ugly
ERROR=_pulsar.LoggerLevel.Error
WARN=_pulsar.LoggerLevel.Warn
INFO=_pulsar.LoggerLevel.Info
DEBUG=_pulsar.LoggerLevel.Debug
class AgentClient(BaseClient):
def __init__(
self, log_level=ERROR,
subscriber=None,
input_queue=None,
output_queue=None,
pulsar_host="pulsar://pulsar:6650",
):
if input_queue is None: input_queue = agent_request_queue
if output_queue is None: output_queue = agent_response_queue
super(AgentClient, self).__init__(
log_level=log_level,
subscriber=subscriber,
input_queue=input_queue,
output_queue=output_queue,
pulsar_host=pulsar_host,
input_schema=AgentRequest,
output_schema=AgentResponse,
)
def request(
self,
question,
think=None,
observe=None,
timeout=300
):
def inspect(x):
if x.thought and think:
think(x.thought)
return
if x.observation and observe:
observe(x.observation)
return
if x.answer:
return True
return False
return self.call(
question=question, inspect=inspect, timeout=timeout
).answer

View file

@ -59,10 +59,14 @@ class BaseClient:
def call(self, **args):
timeout = args.get("timeout", DEFAULT_TIMEOUT)
inspect = args.get("inspect", lambda x: True)
if "timeout" in args:
del args["timeout"]
if "inspect" in args:
del args["inspect"]
id = str(uuid.uuid4())
r = self.input_schema(**args)
@ -103,6 +107,10 @@ class BaseClient:
f"{value.error.type}: {value.error.message}"
)
complete = inspect(value)
if not complete: continue
resp = msg.value()
self.consumer.acknowledge(msg)
return resp

View file

@ -38,7 +38,7 @@ class DocumentRagClient(BaseClient):
output_schema=DocumentRagResponse,
)
def request(self, query, timeout=500):
def request(self, query, timeout=300):
return self.call(
query=query, timeout=timeout

View file

@ -38,8 +38,12 @@ class GraphEmbeddingsClient(BaseClient):
output_schema=GraphEmbeddingsResponse,
)
def request(self, vectors, limit=10, timeout=300):
def request(
self, vectors, user="trustgraph", collection="default",
limit=10, timeout=300
):
return self.call(
user=user, collection=collection,
vectors=vectors, limit=limit, timeout=timeout
).entities

View file

@ -38,9 +38,12 @@ class GraphRagClient(BaseClient):
output_schema=GraphRagResponse,
)
def request(self, query, timeout=500):
def request(
self, query, user="trustgraph", collection="default",
timeout=500
):
return self.call(
query=query, timeout=timeout
user=user, collection=collection, query=query, timeout=timeout
).response

View file

@ -35,6 +35,8 @@ class LlmClient(BaseClient):
output_schema=TextCompletionResponse,
)
def request(self, prompt, timeout=300):
return self.call(prompt=prompt, timeout=timeout).response
def request(self, system, prompt, timeout=300):
return self.call(
system=system, prompt=prompt, timeout=timeout
).response

View file

@ -1,7 +1,9 @@
import _pulsar
import json
import dataclasses
from .. schema import PromptRequest, PromptResponse, Fact, RowSchema, Field
from .. schema import PromptRequest, PromptResponse
from .. schema import prompt_request_queue
from .. schema import prompt_response_queue
from . base import BaseClient
@ -12,6 +14,23 @@ WARN=_pulsar.LoggerLevel.Warn
INFO=_pulsar.LoggerLevel.Info
DEBUG=_pulsar.LoggerLevel.Debug
@dataclasses.dataclass
class Definition:
name: str
definition: str
@dataclasses.dataclass
class Relationship:
s: str
p: str
o: str
o_entity: str
@dataclasses.dataclass
class Topic:
name: str
definition: str
class PromptClient(BaseClient):
def __init__(
@ -38,63 +57,116 @@ class PromptClient(BaseClient):
output_schema=PromptResponse,
)
def request(self, id, variables, timeout=300):
resp = self.call(
id=id,
terms={
k: json.dumps(v)
for k, v in variables.items()
},
timeout=timeout
)
if resp.text: return resp.text
return json.loads(resp.object)
def request_definitions(self, chunk, timeout=300):
return self.call(
kind="extract-definitions", chunk=chunk,
defs = self.request(
id="extract-definitions",
variables={
"text": chunk
},
timeout=timeout
).definitions
def request_topics(self, chunk, timeout=300):
)
return self.call(
kind="extract-topics", chunk=chunk,
timeout=timeout
).topics
return [
Definition(name=d["entity"], definition=d["definition"])
for d in defs
]
def request_relationships(self, chunk, timeout=300):
return self.call(
kind="extract-relationships", chunk=chunk,
rels = self.request(
id="extract-relationships",
variables={
"text": chunk
},
timeout=timeout
).relationships
)
return [
Relationship(
s=d["subject"],
p=d["predicate"],
o=d["object"],
o_entity=d["object-entity"]
)
for d in rels
]
def request_topics(self, chunk, timeout=300):
topics = self.request(
id="extract-topics",
variables={
"text": chunk
},
timeout=timeout
)
return [
Topic(name=d["topic"], definition=d["definition"])
for d in topics
]
def request_rows(self, schema, chunk, timeout=300):
return self.call(
kind="extract-rows", chunk=chunk,
row_schema=RowSchema(
name=schema.name,
description=schema.description,
fields=[
Field(
name=f.name, type=str(f.type), size=f.size,
primary=f.primary, description=f.description,
)
for f in schema.fields
]
),
return self.request(
id="extract-rows",
variables={
"chunk": chunk,
"row-schema": {
"name": schema.name,
"description": schema.description,
"fields": [
{
"name": f.name, "type": str(f.type),
"size": f.size, "primary": f.primary,
"description": f.description,
}
for f in schema.fields
]
}
},
timeout=timeout
).rows
)
def request_kg_prompt(self, query, kg, timeout=300):
return self.call(
kind="kg-prompt",
query=query,
kg=[
Fact(s=v[0], p=v[1], o=v[2])
for v in kg
],
return self.request(
id="kg-prompt",
variables={
"query": query,
"knowledge": [
{ "s": v[0], "p": v[1], "o": v[2] }
for v in kg
]
},
timeout=timeout
).answer
)
def request_document_prompt(self, query, documents, timeout=300):
return self.call(
kind="document-prompt",
query=query,
documents=documents,
return self.request(
id="document-prompt",
variables={
"query": query,
"documents": documents,
},
timeout=timeout
).answer
)

View file

@ -48,11 +48,18 @@ class TriplesQueryClient(BaseClient):
return Value(value=ent, is_uri=False)
def request(self, s, p, o, limit=10, timeout=60):
def request(
self,
s, p, o,
user="trustgraph", collection="default",
limit=10, timeout=120,
):
return self.call(
s=self.create_value(s),
p=self.create_value(p),
o=self.create_value(o),
user=user,
collection=collection,
limit=limit,
timeout=timeout,
).triples

View file

@ -0,0 +1,6 @@
from . identifier import *
from . publication import *
from . document import *
from . organization import *

View file

@ -0,0 +1,25 @@
IS_A = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
LABEL = 'http://www.w3.org/2000/01/rdf-schema#label'
DIGITAL_DOCUMENT = 'https://schema.org/DigitalDocument'
PUBLICATION_EVENT = 'https://schema.org/PublicationEvent'
ORGANIZATION = 'https://schema.org/Organization'
NAME = 'https://schema.org/name'
DESCRIPTION = 'https://schema.org/description'
COPYRIGHT_NOTICE = 'https://schema.org/copyrightNotice'
COPYRIGHT_HOLDER = 'https://schema.org/copyrightHolder'
COPYRIGHT_YEAR = 'https://schema.org/copyrightYear'
LICENSE = 'https://schema.org/license'
PUBLICATION = 'https://schema.org/publication'
START_DATE = 'https://schema.org/startDate'
END_DATE = 'https://schema.org/endDate'
PUBLISHED_BY = 'https://schema.org/publishedBy'
DATE_PUBLISHED = 'https://schema.org/datePublished'
PUBLICATION = 'https://schema.org/publication'
DATE_PUBLISHED = 'https://schema.org/datePublished'
URL = 'https://schema.org/url'
IDENTIFIER = 'https://schema.org/identifier'
KEYWORD = 'https://schema.org/keywords'

View file

@ -0,0 +1,120 @@
from . defs import *
from .. schema import Triple, Value
class DigitalDocument:
def __init__(
self, id, name=None, description=None, copyright_notice=None,
copyright_holder=None, copyright_year=None, license=None,
identifier=None,
publication=None, url=None, keywords=[]
):
self.id = id
self.name = name
self.description = description
self.copyright_notice = copyright_notice
self.copyright_holder = copyright_holder
self.copyright_year = copyright_year
self.license = license
self.publication = publication
self.url = url
self.identifier = identifier
self.keywords = keywords
def emit(self, emit):
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=IS_A, is_uri=True),
o=Value(value=DIGITAL_DOCUMENT, is_uri=True)
))
if self.name:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=LABEL, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=NAME, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
if self.identifier:
emit(Triple(
s=Value(value=id, is_uri=True),
p=Value(value=IDENTIFIER, is_uri=True),
o=Value(value=self.identifier, is_uri=False)
))
if self.description:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=DESCRIPTION, is_uri=True),
o=Value(value=self.description, is_uri=False)
))
if self.copyright_notice:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=COPYRIGHT_NOTICE, is_uri=True),
o=Value(value=self.copyright_notice, is_uri=False)
))
if self.copyright_holder:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=COPYRIGHT_HOLDER, is_uri=True),
o=Value(value=self.copyright_holder, is_uri=False)
))
if self.copyright_year:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=COPYRIGHT_YEAR, is_uri=True),
o=Value(value=self.copyright_year, is_uri=False)
))
if self.license:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=LICENSE, is_uri=True),
o=Value(value=self.license, is_uri=False)
))
if self.keywords:
for k in self.keywords:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=KEYWORD, is_uri=True),
o=Value(value=k, is_uri=False)
))
if self.publication:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=PUBLICATION, is_uri=True),
o=Value(value=self.publication.id, is_uri=True)
))
self.publication.emit(emit)
if self.url:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=URL, is_uri=True),
o=Value(value=self.url, is_uri=True)
))

View file

@ -0,0 +1,23 @@
import uuid
import hashlib
def hash(data):
if isinstance(data, str):
data = data.encode("utf-8")
# Create a SHA256 hash from the data
id = hashlib.sha256(data).hexdigest()
# Convert into a UUID, 64-byte hash becomes 32-byte UUID
id = str(uuid.UUID(id[::2]))
return id
def to_uri(pref, id):
return f"https://trustgraph.ai/{pref}/{id}"
PREF_PUBEV = "pubev"
PREF_ORG = "org"
PREF_DOC = "doc"

View file

@ -0,0 +1,40 @@
from . defs import *
from .. schema import Triple, Value
class Organization:
def __init__(self, id, name=None, description=None):
self.id = id
self.name = name
self.description = description
def emit(self, emit):
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=IS_A, is_uri=True),
o=Value(value=ORGANIZATION, is_uri=True)
))
if self.name:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=LABEL, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=NAME, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
if self.description:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=DESCRIPTION, is_uri=True),
o=Value(value=self.description, is_uri=False)
))

View file

@ -0,0 +1,69 @@
from . defs import *
from .. schema import Triple, Value
class PublicationEvent:
def __init__(
self, id, organization=None, name=None, description=None,
start_date=None, end_date=None,
):
self.id = id
self.organization = organization
self.name = name
self.description = description
self.start_date = start_date
self.end_date = end_date
def emit(self, emit):
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=IS_A, is_uri=True),
o=Value(value=PUBLICATION_EVENT, is_uri=True)))
if self.name:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=LABEL, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=NAME, is_uri=True),
o=Value(value=self.name, is_uri=False)
))
if self.description:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=DESCRIPTION, is_uri=True),
o=Value(value=self.description, is_uri=False)
))
if self.organization:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=PUBLISHED_BY, is_uri=True),
o=Value(value=self.organization.id, is_uri=True)
))
self.organization.emit(emit)
if self.start_date:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=START_DATE, is_uri=True),
o=Value(value=self.start_date, is_uri=False)
))
if self.end_date:
emit(Triple(
s=Value(value=self.id, is_uri=True),
p=Value(value=END_DATE, is_uri=True),
o=Value(value=self.end_date, is_uri=False)))

View file

@ -1,6 +1,7 @@
RDF_LABEL = "http://www.w3.org/2000/01/rdf-schema#label"
DEFINITION = "http://www.w3.org/2004/02/skos/core#definition"
SUBJECT_OF = "https://schema.org/subjectOf"
TRUSTGRAPH_ENTITIES = "http://trustgraph.ai/e/"

View file

@ -7,6 +7,6 @@ from . object import *
from . topic import *
from . graph import *
from . retrieval import *
from . metadata import *
from . agent import *

View file

@ -0,0 +1,37 @@
from pulsar.schema import Record, String, Array, Map
from . topic import topic
from . types import Error
############################################################################
# Prompt services, abstract the prompt generation
class AgentStep(Record):
thought = String()
action = String()
arguments = Map(String())
observation = String()
class AgentRequest(Record):
question = String()
plan = String()
state = String()
history = Array(AgentStep())
class AgentResponse(Record):
answer = String()
error = Error()
thought = String()
observation = String()
agent_request_queue = topic(
'agent', kind='non-persistent', namespace='request'
)
agent_response_queue = topic(
'agent', kind='non-persistent', namespace='response'
)
############################################################################

View file

@ -2,17 +2,13 @@
from pulsar.schema import Record, Bytes, String, Boolean, Integer, Array, Double
from . topic import topic
from . types import Error
class Source(Record):
source = String()
id = String()
title = String()
from . metadata import Metadata
############################################################################
# PDF docs etc.
class Document(Record):
source = Source()
metadata = Metadata()
data = Bytes()
document_ingest_queue = topic('document-load')
@ -22,7 +18,7 @@ document_ingest_queue = topic('document-load')
# Text documents / text from PDF
class TextDocument(Record):
source = Source()
metadata = Metadata()
text = Bytes()
text_ingest_queue = topic('text-document-load')
@ -32,7 +28,7 @@ text_ingest_queue = topic('text-document-load')
# Chunks of text
class Chunk(Record):
source = Source()
metadata = Metadata()
chunk = Bytes()
chunk_ingest_queue = topic('chunk-load')
@ -42,7 +38,7 @@ chunk_ingest_queue = topic('chunk-load')
# Chunk embeddings are an embeddings associated with a text chunk
class ChunkEmbeddings(Record):
source = Source()
metadata = Metadata()
vectors = Array(Array(Double()))
chunk = Bytes()

View file

@ -1,16 +1,16 @@
from pulsar.schema import Record, Bytes, String, Boolean, Integer, Array, Double
from . documents import Source
from . types import Error, Value
from . types import Error, Value, Triple
from . topic import topic
from . metadata import Metadata
############################################################################
# Graph embeddings are embeddings associated with a graph entity
class GraphEmbeddings(Record):
source = Source()
metadata = Metadata()
vectors = Array(Array(Double()))
entity = Value()
@ -23,6 +23,8 @@ graph_embeddings_store_queue = topic('graph-embeddings-store')
class GraphEmbeddingsRequest(Record):
vectors = Array(Array(Double()))
limit = Integer()
user = String()
collection = String()
class GraphEmbeddingsResponse(Record):
error = Error()
@ -39,11 +41,9 @@ graph_embeddings_response_queue = topic(
# Graph triples
class Triple(Record):
source = Source()
s = Value()
p = Value()
o = Value()
class Triples(Record):
metadata = Metadata()
triples = Array(Triple())
triples_store_queue = topic('triples-store')
@ -56,6 +56,8 @@ class TriplesQueryRequest(Record):
p = Value()
o = Value()
limit = Integer()
user = String()
collection = String()
class TriplesQueryResponse(Record):
error = Error()

View file

@ -0,0 +1,16 @@
from pulsar.schema import Record, String, Array
from . types import Triple
class Metadata(Record):
# Source identifier
id = String()
# Subgraph
metadata = Array(Triple())
# Collection management
user = String()
collection = String()

View file

@ -9,6 +9,7 @@ from . types import Error
# LLM text completion
class TextCompletionRequest(Record):
system = String()
prompt = String()
class TextCompletionResponse(Record):

View file

@ -2,7 +2,7 @@
from pulsar.schema import Record, Bytes, String, Boolean, Integer, Array
from pulsar.schema import Double, Map
from . documents import Source
from . metadata import Metadata
from . types import Value, RowSchema
from . topic import topic
@ -12,7 +12,7 @@ from . topic import topic
# object
class ObjectEmbeddings(Record):
source = Source()
metadata = Metadata()
vectors = Array(Array(Double()))
name = String()
key_name = String()
@ -25,7 +25,7 @@ object_embeddings_store_queue = topic('object-embeddings-store')
# Stores rows of information
class Rows(Record):
source = Source()
metadata = Metadata()
row_schema = RowSchema()
rows = Array(Map(String()))

View file

@ -39,20 +39,21 @@ class Fact(Record):
# schema, chunk -> rows
class PromptRequest(Record):
kind = String()
chunk = String()
query = String()
kg = Array(Fact())
documents = Array(Bytes())
row_schema = RowSchema()
id = String()
# JSON encoded values
terms = Map(String())
class PromptResponse(Record):
# Error case
error = Error()
answer = String()
definitions = Array(Definition())
topics = Array(Topic())
relationships = Array(Relationship())
rows = Array(Map(String()))
# Just plain text
text = String()
# JSON encoded
object = String()
prompt_request_queue = topic(
'prompt', kind='non-persistent', namespace='request'

View file

@ -9,6 +9,8 @@ from . types import Error, Value
class GraphRagQuery(Record):
query = String()
user = String()
collection = String()
class GraphRagResponse(Record):
error = Error()
@ -27,6 +29,8 @@ graph_rag_response_queue = topic(
class DocumentRagQuery(Record):
query = String()
user = String()
collection = String()
class DocumentRagResponse(Record):
error = Error()

View file

@ -10,6 +10,11 @@ class Value(Record):
is_uri = Boolean()
type = String()
class Triple(Record):
s = Value()
p = Value()
o = Value()
class Field(Record):
name = String()
# int, string, long, bool, float, double

View file

@ -34,7 +34,7 @@ setuptools.setup(
python_requires='>=3.8',
download_url = "https://github.com/trustgraph-ai/trustgraph/archive/refs/tags/v" + version + ".tar.gz",
install_requires=[
"trustgraph-base<0.12",
"trustgraph-base>=0.15,<0.16",
"pulsar-client",
"prometheus-client",
"boto3",

View file

@ -7,6 +7,7 @@ Input is prompt, output is response. Mistral is default.
import boto3
import json
from prometheus_client import Histogram
import os
from .... schema import TextCompletionRequest, TextCompletionResponse, Error
from .... schema import text_completion_request_queue
@ -21,10 +22,11 @@ default_input_queue = text_completion_request_queue
default_output_queue = text_completion_response_queue
default_subscriber = module
default_model = 'mistral.mistral-large-2407-v1:0'
default_region = 'us-west-2'
default_temperature = 0.0
default_max_output = 2048
default_aws_id_key = os.getenv("AWS_ID_KEY", None)
default_aws_secret = os.getenv("AWS_SECRET", None)
default_aws_region = os.getenv("AWS_REGION", 'us-west-2')
class Processor(ConsumerProducer):
@ -34,12 +36,21 @@ class Processor(ConsumerProducer):
output_queue = params.get("output_queue", default_output_queue)
subscriber = params.get("subscriber", default_subscriber)
model = params.get("model", default_model)
aws_id = params.get("aws_id_key")
aws_secret = params.get("aws_secret")
aws_region = params.get("aws_region", default_region)
aws_id_key = params.get("aws_id_key", default_aws_id_key)
aws_secret = params.get("aws_secret", default_aws_secret)
aws_region = params.get("aws_region", default_aws_region)
temperature = params.get("temperature", default_temperature)
max_output = params.get("max_output", default_max_output)
if aws_id_key is None:
raise RuntimeError("AWS ID not specified")
if aws_secret is None:
raise RuntimeError("AWS secret not specified")
if aws_region is None:
raise RuntimeError("AWS region not specified")
super(Processor, self).__init__(
**params | {
"input_queue": input_queue,
@ -71,7 +82,7 @@ class Processor(ConsumerProducer):
self.max_output = max_output
self.session = boto3.Session(
aws_access_key_id=aws_id,
aws_access_key_id=aws_id_key,
aws_secret_access_key=aws_secret,
region_name=aws_region
)
@ -90,7 +101,7 @@ class Processor(ConsumerProducer):
print(f"Handling prompt {id}...", flush=True)
prompt = v.prompt
prompt = v.system + "\n\n" + v.prompt
try:
@ -289,17 +300,20 @@ class Processor(ConsumerProducer):
parser.add_argument(
'-z', '--aws-id-key',
default=default_aws_id_key,
help=f'AWS ID Key'
)
parser.add_argument(
'-k', '--aws-secret',
default=default_aws_secret,
help=f'AWS Secret Key'
)
parser.add_argument(
'-r', '--aws-region',
help=f'AWS Region (default: us-west-2)'
default=default_aws_region,
help=f'AWS Region'
)
parser.add_argument(
@ -320,4 +334,3 @@ def run():
Processor.start(module, __doc__)

View file

@ -9,12 +9,17 @@ import os
from trustgraph.clients.triples_query_client import TriplesQueryClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
default_user = 'trustgraph'
default_collection = 'default'
def show_graph(pulsar):
def show_graph(pulsar, user, collection):
tq = TriplesQueryClient(pulsar_host=pulsar)
rows = tq.request(None, None, None, limit=10_000_000)
rows = tq.request(
user=user, collection=collection,
s=None, p=None, o=None, limit=10_000_000
)
for row in rows:
print(row.s.value, row.p.value, row.o.value)
@ -22,7 +27,7 @@ def show_graph(pulsar):
def main():
parser = argparse.ArgumentParser(
prog='graph-show',
prog='tg-graph-show',
description=__doc__,
)
@ -32,11 +37,26 @@ def main():
help=f'Pulsar host (default: {default_pulsar_host})',
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
args = parser.parse_args()
try:
show_graph(args.pulsar_host)
show_graph(
pulsar=args.pulsar_host, user=args.user,
collection=args.collection,
)
except Exception as e:

View file

@ -1,7 +1,8 @@
#!/usr/bin/env python3
"""
Connects to the graph query service and dumps all graph edges.
Connects to the graph query service and dumps all graph edges in Turtle
format.
"""
import argparse
@ -50,7 +51,7 @@ def show_graph(pulsar):
def main():
parser = argparse.ArgumentParser(
prog='graph-show',
prog='tg-graph-to-turtle',
description=__doc__,
)

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Initialises Pulsar with Trustgraph tenant / namespaces & policy
Initialises Pulsar with Trustgraph tenant / namespaces & policy.
"""
import requests

View file

@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Uses the GraphRAG service to answer a query
"""
import argparse
import os
import textwrap
from trustgraph.clients.agent_client import AgentClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
default_user = 'trustgraph'
default_collection = 'default'
def wrap(text, width=75):
if text is None: text = "n/a"
out = textwrap.wrap(
text, width=width
)
return "\n".join(out)
def output(text, prefix="> ", width=78):
out = textwrap.indent(
text, prefix=prefix
)
print(out)
def query(
pulsar_host, query, user, collection,
plan=None, state=None, verbose=False
):
am = AgentClient(pulsar_host=pulsar_host)
if verbose:
output(wrap(query), "\U00002753 ")
print()
def think(x):
if verbose:
output(wrap(x), "\U0001f914 ")
print()
def observe(x):
if verbose:
output(wrap(x), "\U0001f4a1 ")
print()
resp = am.request(
question=query, think=think, observe=observe,
)
print(resp)
def main():
parser = argparse.ArgumentParser(
prog='tg-invoke-agent',
description=__doc__,
)
parser.add_argument(
'-p', '--pulsar-host',
default=default_pulsar_host,
help=f'Pulsar host (default: {default_pulsar_host})',
)
parser.add_argument(
'-q', '--query',
required=True,
help=f'Query to execute',
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
parser.add_argument(
'-l', '--plan',
help=f'Agent plan (default: unspecified)'
)
parser.add_argument(
'-s', '--state',
help=f'Agent initial state (default: unspecified)'
)
parser.add_argument(
'-v', '--verbose',
action="store_true",
help=f'Output thinking/observations'
)
args = parser.parse_args()
try:
query(
pulsar_host=args.pulsar_host,
query=args.query,
user=args.user,
collection=args.collection,
plan=args.plan,
state=args.state,
verbose=args.verbose,
)
except Exception as e:
print("Exception:", e, flush=True)
main()

View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Invokes the text completion service by specifying an LLM system prompt
and user prompt. Both arguments are required.
"""
import argparse
import os
import json
from trustgraph.clients.llm_client import LlmClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
def query(pulsar_host, system, prompt):
cli = LlmClient(pulsar_host=pulsar_host)
resp = cli.request(system=system, prompt=prompt)
print(resp)
def main():
parser = argparse.ArgumentParser(
prog='tg-invoke-llm',
description=__doc__,
)
parser.add_argument(
'-p', '--pulsar-host',
default=default_pulsar_host,
help=f'Pulsar host (default: {default_pulsar_host})',
)
parser.add_argument(
'system',
nargs=1,
help='LLM system prompt e.g. You are a helpful assistant',
)
parser.add_argument(
'prompt',
nargs=1,
help='LLM prompt e.g. What is 2 + 2?',
)
args = parser.parse_args()
try:
query(
pulsar_host=args.pulsar_host,
system=args.system[0],
prompt=args.prompt[0],
)
except Exception as e:
print("Exception:", e, flush=True)
main()

View file

@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Invokes the LLM prompt service by specifying the prompt template to use
and values for the variables in the prompt template. The
prompt template is identified by its template identifier e.g.
question, extract-definitions. Template variable values are specified
using key=value arguments on the command line, and these replace
{{key}} placeholders in the template.
"""
import argparse
import os
import json
from trustgraph.clients.prompt_client import PromptClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
def query(pulsar_host, template_id, variables):
cli = PromptClient(pulsar_host=pulsar_host)
resp = cli.request(id=template_id, variables=variables)
if isinstance(resp, str):
print(resp)
else:
print(json.dumps(resp, indent=4))
def main():
parser = argparse.ArgumentParser(
prog='tg-invoke-prompt',
description=__doc__,
)
parser.add_argument(
'-p', '--pulsar-host',
default=default_pulsar_host,
help=f'Pulsar host (default: {default_pulsar_host})',
)
parser.add_argument(
'id',
metavar='template-id',
nargs=1,
help=f'Prompt identifier e.g. question, extract-definitions',
)
parser.add_argument(
'variable',
nargs='*',
metavar="variable=value",
help='''Prompt template terms of the form variable=value, can be
specified multiple times''',
)
args = parser.parse_args()
variables = {}
for variable in args.variable:
toks = variable.split("=", 1)
if len(toks) != 2:
raise RuntimeError(f"Malformed variable: {variable}")
variables[toks[0]] = toks[1]
try:
query(
pulsar_host=args.pulsar_host,
template_id=args.id[0],
variables=variables,
)
except Exception as e:
print("Exception:", e, flush=True)
main()

View file

@ -6,14 +6,23 @@ Loads a PDF document into TrustGraph processing.
import pulsar
from pulsar.schema import JsonSchema
from trustgraph.schema import Document, Source, document_ingest_queue
import base64
import hashlib
import argparse
import os
import time
import uuid
from trustgraph.schema import Document, document_ingest_queue
from trustgraph.schema import Metadata
from trustgraph.log_level import LogLevel
from trustgraph.knowledge import hash, to_uri
from trustgraph.knowledge import PREF_PUBEV, PREF_DOC, PREF_ORG
from trustgraph.knowledge import Organization, PublicationEvent
from trustgraph.knowledge import DigitalDocument
default_user = 'trustgraph'
default_collection = 'default'
class Loader:
@ -21,7 +30,10 @@ class Loader:
self,
pulsar_host,
output_queue,
user,
collection,
log_level,
metadata,
):
self.client = pulsar.Client(
@ -35,6 +47,10 @@ class Loader:
chunking_enabled=True,
)
self.user = user
self.collection = collection
self.metadata = metadata
def load(self, files):
for file in files:
@ -47,13 +63,25 @@ class Loader:
path = file
data = open(path, "rb").read()
id = hashlib.sha256(path.encode("utf-8")).hexdigest()[0:8]
# Create a SHA256 hash from the data
id = hash(data)
id = to_uri(PREF_DOC, id)
triples = []
def emit(t):
triples.append(t)
self.metadata.id = id
self.metadata.emit(emit)
r = Document(
source=Source(
source=path,
title=path,
metadata=Metadata(
id=id,
metadata=triples,
user=self.user,
collection=self.collection,
),
data=base64.b64encode(data),
)
@ -71,7 +99,7 @@ class Loader:
def main():
parser = argparse.ArgumentParser(
prog='loader',
prog='tg-load-pdf',
description=__doc__,
)
@ -90,6 +118,66 @@ def main():
help=f'Output queue (default: {default_output_queue})'
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
parser.add_argument(
'--name', help=f'Document name'
)
parser.add_argument(
'--description', help=f'Document description'
)
parser.add_argument(
'--copyright-notice', help=f'Copyright notice'
)
parser.add_argument(
'--copyright-holder', help=f'Copyright holder'
)
parser.add_argument(
'--copyright-year', help=f'Copyright year'
)
parser.add_argument(
'--license', help=f'Copyright license'
)
parser.add_argument(
'--publication-organization', help=f'Publication organization'
)
parser.add_argument(
'--publication-description', help=f'Publication description'
)
parser.add_argument(
'--publication-date', help=f'Publication date'
)
parser.add_argument(
'--url', help=f'Document URL'
)
parser.add_argument(
'--keyword', nargs='+', help=f'Keyword'
)
parser.add_argument(
'--identifier', '--id', help=f'Document ID'
)
parser.add_argument(
'-l', '--log-level',
type=LogLevel,
@ -109,10 +197,38 @@ def main():
try:
document = DigitalDocument(
id,
name=args.name,
description=args.description,
copyright_notice=args.copyright_notice,
copyright_holder=args.copyright_holder,
copyright_year=args.copyright_year,
license=args.license,
url=args.url,
keywords=args.keyword,
)
if args.publication_organization:
org = Organization(
id=to_uri(PREF_ORG, hash(args.publication_organization)),
name=args.publication_organization,
)
document.publication = PublicationEvent(
id = to_uri(PREF_PUBEV, str(uuid.uuid4())),
organization=org,
description=args.publication_description,
start_date=args.publication_date,
end_date=args.publication_date,
)
p = Loader(
pulsar_host=args.pulsar_host,
output_queue=args.output_queue,
user=args.user,
collection=args.collection,
log_level=args.log_level,
metadata=document,
)
p.load(args.files)

View file

@ -6,14 +6,23 @@ Loads a text document into TrustGraph processing.
import pulsar
from pulsar.schema import JsonSchema
from trustgraph.schema import TextDocument, Source, text_ingest_queue
import base64
import hashlib
import argparse
import os
import time
import uuid
from trustgraph.schema import TextDocument, text_ingest_queue
from trustgraph.schema import Metadata
from trustgraph.log_level import LogLevel
from trustgraph.knowledge import hash, to_uri
from trustgraph.knowledge import PREF_PUBEV, PREF_DOC, PREF_ORG
from trustgraph.knowledge import Organization, PublicationEvent
from trustgraph.knowledge import DigitalDocument
default_user = 'trustgraph'
default_collection = 'default'
class Loader:
@ -21,7 +30,10 @@ class Loader:
self,
pulsar_host,
output_queue,
user,
collection,
log_level,
metadata,
):
self.client = pulsar.Client(
@ -35,6 +47,10 @@ class Loader:
chunking_enabled=True,
)
self.user = user
self.collection = collection
self.metadata = metadata
def load(self, files):
for file in files:
@ -47,13 +63,25 @@ class Loader:
path = file
data = open(path, "rb").read()
id = hashlib.sha256(path.encode("utf-8")).hexdigest()[0:8]
# Create a SHA256 hash from the data
id = hash(data)
id = to_uri(PREF_DOC, id)
triples = []
def emit(t):
triples.append(t)
self.metadata.id = id
self.metadata.emit(emit)
r = TextDocument(
source=Source(
source=path,
title=path,
metadata=Metadata(
id=id,
metadata=triples,
user=self.user,
collection=self.collection,
),
text=data,
)
@ -71,7 +99,7 @@ class Loader:
def main():
parser = argparse.ArgumentParser(
prog='loader',
prog='tg-load-text',
description=__doc__,
)
@ -90,6 +118,66 @@ def main():
help=f'Output queue (default: {default_output_queue})'
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
parser.add_argument(
'--name', help=f'Document name'
)
parser.add_argument(
'--description', help=f'Document description'
)
parser.add_argument(
'--copyright-notice', help=f'Copyright notice'
)
parser.add_argument(
'--copyright-holder', help=f'Copyright holder'
)
parser.add_argument(
'--copyright-year', help=f'Copyright year'
)
parser.add_argument(
'--license', help=f'Copyright license'
)
parser.add_argument(
'--publication-organization', help=f'Publication organization'
)
parser.add_argument(
'--publication-description', help=f'Publication description'
)
parser.add_argument(
'--publication-date', help=f'Publication date'
)
parser.add_argument(
'--url', help=f'Document URL'
)
parser.add_argument(
'--keyword', nargs='+', help=f'Keyword'
)
parser.add_argument(
'--identifier', '--id', help=f'Document ID'
)
parser.add_argument(
'-l', '--log-level',
type=LogLevel,
@ -109,10 +197,38 @@ def main():
try:
document = DigitalDocument(
id,
name=args.name,
description=args.description,
copyright_notice=args.copyright_notice,
copyright_holder=args.copyright_holder,
copyright_year=args.copyright_year,
license=args.license,
url=args.url,
keywords=args.keyword,
)
if args.publication_organization:
org = Organization(
id=to_uri(PREF_ORG, hash(args.publication_organization)),
name=args.publication_organization,
)
document.publication = PublicationEvent(
id = to_uri(PREF_PUBEV, str(uuid.uuid4())),
organization=org,
description=args.publication_description,
start_date=args.publication_date,
end_date=args.publication_date,
)
p = Loader(
pulsar_host=args.pulsar_host,
output_queue=args.output_queue,
user=args.user,
collection=args.collection,
log_level=args.log_level,
metadata=document,
)
p.load(args.files)

View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""
Loads Graph embeddings into TrustGraph processing.
"""
import pulsar
from pulsar.schema import JsonSchema
from trustgraph.schema import Triples, Triple, Value, Metadata
from trustgraph.schema import triples_store_queue
import argparse
import os
import time
import pyarrow as pa
import rdflib
from trustgraph.log_level import LogLevel
default_user = 'trustgraph'
default_collection = 'default'
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
default_output_queue = triples_store_queue
class Loader:
def __init__(
self,
pulsar_host,
output_queue,
log_level,
files,
user,
collection,
):
self.client = pulsar.Client(
pulsar_host,
logger=pulsar.ConsoleLogger(log_level.to_pulsar())
)
self.producer = self.client.create_producer(
topic=output_queue,
schema=JsonSchema(Triples),
chunking_enabled=True,
)
self.files = files
self.user = user
self.collection = collection
def run(self):
try:
for file in self.files:
self.load_file(file)
except Exception as e:
print(e, flush=True)
def load_file(self, file):
g = rdflib.Graph()
g.parse(file, format="turtle")
for e in g:
s = Value(value=str(e[0]), is_uri=True)
p = Value(value=str(e[1]), is_uri=True)
if type(e[2]) == rdflib.term.URIRef:
o = Value(value=str(e[2]), is_uri=True)
else:
o = Value(value=str(e[2]), is_uri=False)
r = Triples(
metadata=Metadata(
id=None,
metadata=[],
user=self.user,
collection=self.collection,
),
triples=[ Triple(s=s, p=p, o=o) ]
)
self.producer.send(r)
def __del__(self):
self.client.close()
def main():
parser = argparse.ArgumentParser(
prog='tg-load-turtle',
description=__doc__,
)
parser.add_argument(
'-p', '--pulsar-host',
default=default_pulsar_host,
help=f'Pulsar host (default: {default_pulsar_host})',
)
parser.add_argument(
'-o', '--output-queue',
default=default_output_queue,
help=f'Output queue (default: {default_output_queue})'
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
parser.add_argument(
'-l', '--log-level',
type=LogLevel,
default=LogLevel.ERROR,
choices=list(LogLevel),
help=f'Output queue (default: info)'
)
parser.add_argument(
'files', nargs='+',
help=f'Turtle files to load'
)
args = parser.parse_args()
while True:
try:
p = Loader(
pulsar_host=args.pulsar_host,
output_queue=args.output_queue,
log_level=args.log_level,
files=args.files,
user=args.user,
collection=args.collection,
)
p.run()
print("File loaded.")
break
except Exception as e:
print("Exception:", e, flush=True)
print("Will retry...", flush=True)
time.sleep(10)
main()

View file

@ -9,17 +9,19 @@ import os
from trustgraph.clients.document_rag_client import DocumentRagClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
default_user = 'trustgraph'
default_collection = 'default'
def query(pulsar, query):
def query(pulsar_host, query, user, collection):
rag = DocumentRagClient(pulsar_host=pulsar)
resp = rag.request(query)
resp = rag.request(user=user, collection=collection, query=query)
print(resp)
def main():
parser = argparse.ArgumentParser(
prog='graph-show',
prog='tg-query-document-rag',
description=__doc__,
)
@ -35,11 +37,28 @@ def main():
help=f'Query to execute',
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
args = parser.parse_args()
try:
query(args.pulsar_host, args.query)
query(
pulsar_host=args.pulsar_host,
query=args.query,
user=args.user,
collection=args.collection,
)
except Exception as e:

View file

@ -9,17 +9,19 @@ import os
from trustgraph.clients.graph_rag_client import GraphRagClient
default_pulsar_host = os.getenv("PULSAR_HOST", 'pulsar://localhost:6650')
default_user = 'trustgraph'
default_collection = 'default'
def query(pulsar, query):
def query(pulsar_host, query, user, collection):
rag = GraphRagClient(pulsar_host=pulsar)
resp = rag.request(query)
rag = GraphRagClient(pulsar_host=pulsar_host)
resp = rag.request(user=user, collection=collection, query=query)
print(resp)
def main():
parser = argparse.ArgumentParser(
prog='graph-show',
prog='tg-graph-query-rag',
description=__doc__,
)
@ -35,11 +37,28 @@ def main():
help=f'Query to execute',
)
parser.add_argument(
'-u', '--user',
default=default_user,
help=f'User ID (default: {default_user})'
)
parser.add_argument(
'-c', '--collection',
default=default_collection,
help=f'Collection ID (default: {default_collection})'
)
args = parser.parse_args()
try:
query(args.pulsar_host, args.query)
query(
pulsar_host=args.pulsar_host,
query=args.query,
user=args.user,
collection=args.collection,
)
except Exception as e:

View file

@ -34,7 +34,7 @@ setuptools.setup(
python_requires='>=3.8',
download_url = "https://github.com/trustgraph-ai/trustgraph/archive/refs/tags/v" + version + ".tar.gz",
install_requires=[
"trustgraph-base<0.12",
"trustgraph-base>=0.15,<0.16",
"requests",
"pulsar-client",
"rdflib",
@ -46,9 +46,13 @@ setuptools.setup(
"scripts/tg-init-pulsar-manager",
"scripts/tg-load-pdf",
"scripts/tg-load-text",
"scripts/tg-load-turtle",
"scripts/tg-query-document-rag",
"scripts/tg-query-graph-rag",
"scripts/tg-init-pulsar",
"scripts/tg-processor-state",
"scripts/tg-invoke-agent",
"scripts/tg-invoke-prompt",
"scripts/tg-invoke-llm",
]
)

View file

@ -34,8 +34,8 @@ setuptools.setup(
python_requires='>=3.8',
download_url = "https://github.com/trustgraph-ai/trustgraph/archive/refs/tags/v" + version + ".tar.gz",
install_requires=[
"trustgraph-base<0.12",
"trustgraph-flow<0.12",
"trustgraph-base>=0.15,<0.16",
"trustgraph-flow>=0.15,<0.16",
"torch",
"urllib3",
"transformers",

View file

@ -0,0 +1,6 @@
#!/usr/bin/env python3
from trustgraph.agent.react import run
run()

View file

@ -0,0 +1,6 @@
#!/usr/bin/env python3
from trustgraph.model.text_completion.azure_openai import run
run()

View file

@ -0,0 +1,6 @@
#!/usr/bin/env python3
from trustgraph.model.text_completion.googleaistudio import run
run()

View file

@ -34,7 +34,7 @@ setuptools.setup(
python_requires='>=3.8',
download_url = "https://github.com/trustgraph-ai/trustgraph/archive/refs/tags/v" + version + ".tar.gz",
install_requires=[
"trustgraph-base<0.12",
"trustgraph-base>=0.15,<0.16",
"urllib3",
"rdflib",
"pymilvus",
@ -55,8 +55,12 @@ setuptools.setup(
"openai",
"neo4j",
"tiktoken",
"google-generativeai",
"ibis",
"jsonschema",
],
scripts=[
"scripts/agent-manager-react",
"scripts/chunker-recursive",
"scripts/chunker-token",
"scripts/de-query-milvus",
@ -83,8 +87,10 @@ setuptools.setup(
"scripts/rows-write-cassandra",
"scripts/run-processing",
"scripts/text-completion-azure",
"scripts/text-completion-azure-openai",
"scripts/text-completion-claude",
"scripts/text-completion-cohere",
"scripts/text-completion-googleaistudio",
"scripts/text-completion-llamafile",
"scripts/text-completion-ollama",
"scripts/text-completion-openai",

View file

@ -0,0 +1,19 @@
agent-manager-react \
-p pulsar://localhost:6650 \
--tool-type \
shuttle=knowledge-query:query \
cats=knowledge-query:query \
compute=text-completion:computation \
--tool-description \
shuttle="Query a knowledge base with information about the space shuttle. The query should be a simple natural language question" \
cats="Query a knowledge base with information about Mark's cats. The query should be a simple natural language question" \
compute="A computation engine which can answer questions about maths and computation" \
--tool-argument \
cats="query:string:The search query string" \
shuttle="query:string:The search query string" \
compute="computation:string:The computation to solve"
--context 'The space shuttle challenger final mission was 58-L'

Some files were not shown because too many files have changed in this diff Show more