From 749b0a974bd599edda8571327970d0507378ec67 Mon Sep 17 00:00:00 2001 From: Diego gosmar Date: Thu, 22 May 2025 19:03:11 +0200 Subject: [PATCH 1/3] V3 Open Floor Standard Agentic Compliant Signed-off-by: industrialpoet --- V3/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/V3/README.md b/V3/README.md index d2d8680..5241c62 100644 --- a/V3/README.md +++ b/V3/README.md @@ -10,13 +10,13 @@ https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specific BeaconForge Logo
-The picture shows a user interacting with several assistants, agent assistant a, agent assistant b and agent assistant c, which all communicate through the Open Voice specifications. The code in this repository will enable you to create your own versions of these assistants. +The picture shows a user interacting with several assistants, agent assistant a, agent assistant b and agent assistant c, which all communicate through the Open-Floor specifications. The code in this repository will enable you to create your own versions of these assistants. See please the following Arxiv papers for more information about the specifications:
Link to Agentic Research Paper #1
Link to Multi-party Research Paper #2
The official specifications can be found in
-https://github.com/open-voice-interoperability/docs/tree/main/specifications +https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications # V3 From b18a924d535f392a2a9479191b54690a20106818 Mon Sep 17 00:00:00 2001 From: jQua Date: Fri, 20 Mar 2026 15:49:26 -0400 Subject: [PATCH 2/3] first cut of the v3 php agent Everything you need to build a "hello world" Open Floor Protocol agent in PHP Signed-off-by: industrialpoet --- V3/bfPHP/Parrot/Parrot_test_convo_id.json | 11 + V3/bfPHP/Parrot/Parrot_test_convo_id.log | 1 + V3/bfPHP/README.md | 586 ++++++++++++++++++++ V3/bfPHP/agDef.json | 37 ++ V3/bfPHP/beaconforgeV3.php | 639 ++++++++++++++++++++++ V3/bfPHP/myAgFun.php | 55 ++ V3/bfPHP/run.php | 38 ++ 7 files changed, 1367 insertions(+) create mode 100644 V3/bfPHP/Parrot/Parrot_test_convo_id.json create mode 100644 V3/bfPHP/Parrot/Parrot_test_convo_id.log create mode 100644 V3/bfPHP/README.md create mode 100644 V3/bfPHP/agDef.json create mode 100644 V3/bfPHP/beaconforgeV3.php create mode 100644 V3/bfPHP/myAgFun.php create mode 100644 V3/bfPHP/run.php diff --git a/V3/bfPHP/Parrot/Parrot_test_convo_id.json b/V3/bfPHP/Parrot/Parrot_test_convo_id.json new file mode 100644 index 0000000..f9db83e --- /dev/null +++ b/V3/bfPHP/Parrot/Parrot_test_convo_id.json @@ -0,0 +1,11 @@ +{ + "exchanges": [ + { + "utt": "Hello, how are you?", + "from": "ofpSocketClient", + "toMe": false, + "toSomeoneElse": false, + "said": "I still need a cracker, but I heard Hello, how are you? and I am not sure if it was meant for me." + } + ] +} \ No newline at end of file diff --git a/V3/bfPHP/Parrot/Parrot_test_convo_id.log b/V3/bfPHP/Parrot/Parrot_test_convo_id.log new file mode 100644 index 0000000..f13421b --- /dev/null +++ b/V3/bfPHP/Parrot/Parrot_test_convo_id.log @@ -0,0 +1 @@ +{"convoLog":[[{"time":"03-20-2026_16:15:12","note":"I sent the manifest and said: Manifest sent."}],[{"time":"03-20-2026_16:16:35","note":"I received an invite with reason [ none ] from floorManager. I accepted the invite and said: Thankyou for the invitation. got any crackers?"}],[{"time":"03-20-2026_16:16:44","note":"I heard: Hello, how are you? from ofpSocketClient. toMe? No"}],[{"time":"03-20-2026_16:16:44","note":"I heard: Hello, how are you? from ofpSocketClient. toMe? No"},{"time":"03-20-2026_16:16:44","note":"I said: I still need a cracker, but I heard Hello, how are you? and I am not sure if it was meant for me."}]]} \ No newline at end of file diff --git a/V3/bfPHP/README.md b/V3/bfPHP/README.md new file mode 100644 index 0000000..d1c3f39 --- /dev/null +++ b/V3/bfPHP/README.md @@ -0,0 +1,586 @@ +# BeaconForge PHP — A "Hello World" Open Floor Protocol Agent + +BeaconForge Logo + +> A minimal, deployable PHP implementation of an Open Floor Protocol (OFP) conversational agent. If your hosting site can run WordPress, it can run this agent. + +--- + +## Table of Contents + +- [What is BeaconForge PHP?](#what-is-beaconforge-php) +- [What is the Open Floor Protocol?](#what-is-the-open-floor-protocol) +- [Repository Files](#repository-files) +- [Deploying to a Shared Hosting Site](#deploying-to-a-shared-hosting-site) +- [Modifying agDef.json — The Agent Manifest](#modifying-agdefjson--the-agent-manifest) +- [Customizing myAgFun.php — Your Agent Logic](#customizing-myagfunphp--your-agent-logic) +- [The Parrot Example](#the-parrot-example) +- [Testing with Postman](#testing-with-postman) +- [How It Works (Under the Hood)](#how-it-works-under-the-hood) + +--- + +## What is BeaconForge PHP? + +BeaconForge PHP (`bfPHP`) is a **minimalist, production-ready PHP framework** for creating a conversational agent that participates in an [Open Floor Protocol (OFP)](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications) multi-agent conversation. It is part of the broader [open-voice-interoperability](https://github.com/open-voice-interoperability) project. + +The goal of `bfPHP` is to lower the barrier of entry as far as possible. To build a functional OFP agent you only need to modify **two files**: + +| File | Purpose | +|------|---------| +| `agDef.json` | Describes *who your agent is* — its identity, organization, and capabilities (the OFP manifest) | +| `myAgFun.php` | Implements *what your agent does* — how it responds to being invited to a conversation and to utterances it hears | + +Everything else (`run.php`, `beaconforgeV3.php`) is the framework plumbing that handles OFP message parsing, formatting, routing, and persistence — you do not need to change those files. + +--- + +## What is the Open Floor Protocol? + +The Open Floor Protocol (OFP) is an open standard developed by the [Open Voice Interoperability Initiative — LF AI & Data Foundation](https://github.com/open-voice-interoperability). It defines how conversational agents discover one another and exchange messages using standard HTTP POST requests carrying JSON "conversation envelopes". + +In an OFP conversation: + +- A **floor manager** orchestrates which agents are active in a conversation. +- **Agents** are invited to join, hear utterances, and respond. +- Every agent exposes a single HTTP endpoint that accepts and returns OFP-formatted JSON. +- Agents publish a **manifest** describing their identity and capabilities, which allows discovery agents and floor managers to find the right agent for a task. + +Key specifications: +- [Conversation Envelope Specification 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/ConversationEnvelope/1.0.1) +- [Assistant Manifest Specification 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/AssistantManifest/1.0.1) ← *defines the structure of `agDef.json`* + +--- + +## Repository Files + +``` +bfPHP/ +├── run.php ← HTTP entry point. Upload this to your public directory. +├── beaconforgeV3.php ← Core framework. Do not modify. +├── agDef.json ← Your agent's manifest/definition. Edit this. +├── myAgFun.php ← Your agent's conversational logic. Edit this. +└── Parrot/ ← Example persistent data from a test conversation + ├── Parrot_test_convo_id.json + └── Parrot_test_convo_id.log +``` + +### `run.php` — The HTTP Endpoint + +This is the public-facing entry point. It: +1. Accepts a POST request containing an OFP JSON conversation envelope. +2. Passes it to the `simpleProcessOFP()` function in `beaconforgeV3.php`. +3. Returns the agent's OFP JSON response. + +The two lines you may want to configure in `run.php`: + +```php +$agentFunctionsFileName = 'myAgFun.php'; // Your agent logic file +$agentDefinitionJSON = 'agDef.json'; // Your agent manifest file + +$pathForPersistantStorage = ''; // See "Persistent Storage" below +``` + +### `beaconforgeV3.php` — The Framework + +Contains all the OFP message handling, event parsing, manifest building, and persistent storage logic. This is the engine. You should not need to modify this file. + +### `agDef.json` — The Agent Manifest + +Defines your agent's identity and capabilities in a format compliant with the [OFP Assistant Manifest Specification 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/AssistantManifest/1.0.1). See [Modifying agDef.json](#modifying-agdefjson--the-agent-manifest) for full details. + +### `myAgFun.php` — Your Agent Logic + +A PHP class that extends the base framework. Override the functions here to implement your agent's personality and behavior. See [Customizing myAgFun.php](#customizing-myagfunphp--your-agent-logic) for full details. + +--- + +## Deploying to a Shared Hosting Site + +### Requirements + +- A PHP hosting account (shared hosting is fine — no special server software required) +- Any host that supports WordPress **already meets** the requirements for this agent +- PHP 7.4 or higher (PHP 8.x recommended) + +### Directory Structure + +Place all four PHP/JSON files together in a directory under your web server's `public` folder. A suggested path: + +``` +public/ +└── ov1/ + └── ag/ + └── youragentname/ + ├── run.php + ├── beaconforgeV3.php + ├── agDef.json + └── myAgFun.php +``` + +Your agent's endpoint URL will then be: + +``` +https://yourdomain.com/ov1/ag/youragentname/run.php +``` + +This URL must match the `serviceUrl` field in your `agDef.json` manifest (see below). + +### Uploading Files + +Use your host's file manager or an FTP client (such as FileZilla) to upload all four files to the directory you created. + +Upload button in cPanel file manager + +### Persistent Storage + +The framework automatically saves a JSON log file and a persistence state file for each conversation. These files are written to a subdirectory named after your agent's `conversationalName`. + +**By default** (`$pathForPersistantStorage = ''`), these files are written inside the same directory as your PHP files (i.e., inside your `public` folder). This is acceptable for development and testing. + +**For production**, it is strongly recommended to store these files **outside** your `public` directory so they are not directly web-accessible. To do this, edit the `$pathForPersistantStorage` line in `run.php`: + +```php +// Example: if your public directory is at /home/username/public_html/ +// store persistent data alongside it at /home/username/agentdata/ +$pathForPersistantStorage = '../../../../agentdata/'; +``` + +The framework will automatically create a subdirectory inside that path named after your agent (e.g., `agentdata/Parrot/`). Make sure the path exists and is writable by PHP. + +### Verify It Works + +After uploading, send a test POST request to `run.php` using [Postman](https://www.postman.com/) or `curl`. See [Testing with Postman](#testing-with-postman) for a ready-to-use request. + +--- + +## Modifying agDef.json — The Agent Manifest + +The `agDef.json` file is your agent's identity card. It is read once at startup and its contents are sent to the floor manager when a `getManifests` event is received. + +The file follows the [OFP Assistant Manifest Specification 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/AssistantManifest/1.0.1), wrapped in a `manifest` key for internal use by the framework. + +### Structure + +```json +{ + "manifest": { + "identification": { ... }, + "character": { ... }, + "capabilities": { ... } + } +} +``` + +--- + +### `identification` — Who Your Agent Is + +This section is **mandatory** and defines the core identity of your agent. It is used by floor managers and discovery agents to find and address your agent. + +| Field | Type | Mandatory | Description | +|-------|------|-----------|-------------| +| `speakerUri` | URI string | **Yes** | The unique, permanent identity of your agent. Use a URI format such as `tag:yourdomain.com,2026:myagent`. This never changes even if your endpoint URL changes. | +| `serviceUrl` | URL string | **Yes** | The full URL of your agent's `run.php` endpoint. **Must match your actual hosted URL.** | +| `conversationalName` | string | **Yes** | The name your agent introduces itself by and responds to when addressed by name. Doubles as the name of the persistent storage subdirectory. | +| `organization` | string | **Yes** | The name of the organization this agent represents. | +| `synopsis` | string | **Yes** | One sentence (under ~75 characters) describing what this agent does. Suitable for text-to-speech. | +| `department` | string | No | The area or department within the organization. | +| `role` | string | No | The agent's job title or role (e.g., "Weather Specialist", "Customer Support"). | +| `openFloorRoles` | dict | No | OFP roles this agent can perform. E.g., `{ "convener": true }` or `{ "discovery": true }`. | +| `ALTserviceUrl` | URL string | No | An alternative service URL. The framework will switch to this if a message is directed to it. | + +**Example:** +```json +"identification": { + "serviceUrl": "https://yourdomain.com/ov1/ag/myagent/run.php", + "speakerUri": "tag:yourdomain.com,2026:myagent", + "conversationalName": "Max", + "role": "Customer Support Specialist", + "department": "Support", + "organization": "Acme Corp", + "synopsis": "Customer support assistant for Acme Corp products." +} +``` + +> **Important:** The `speakerUri` is the *permanent identity* of your agent. The `serviceUrl` is *where it lives right now*. You can move your agent to a new URL without changing its identity. + +--- + +### `character` — Voice and Appearance (BeaconForge Extension) + +This section is a BeaconForge-specific extension (not part of the core OFP spec) that hints to compatible clients how to render and speak for this agent. + +```json +"character": { + "headShot": "aParrot.png", + "voice": { + "vendor": "MS_EDGE", + "name": "Zira", + "uri": "Microsoft Zira - English (United States)" + } +} +``` + +--- + +### `capabilities` — What Your Agent Can Do + +This section is **mandatory** and describes the services your agent provides. Discovery agents and floor managers use this to decide whether to invite your agent to handle a particular task. + +Per the [OFP spec](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/AssistantManifest/1.0.1), capabilities should be expressed as an **array** of capability objects (allowing an agent to offer multiple distinct capabilities): + +| Field | Type | Description | +|-------|------|-------------| +| `keyphrases` | string array | **Mandatory.** Short searchable words or phrases that represent the topics your agent handles. Used by simple text-based search. | +| `languages` | string array | Languages supported, as [IETF BCP 47](https://www.rfc-editor.org/rfc/rfc5646.txt) tags (e.g., `"en-us"`, `"de-de"`). | +| `descriptions` | string array | **Mandatory.** Natural language sentences describing what your agent can do. Used by AI-powered discovery. | +| `supportedLayers` | dict of string arrays | The dialog event layers supported for input and output. Standard values: `"text"`, `"voice"`, `"ssml"`. Format: `{ "input": ["text"], "output": ["text"] }` | + +**Example:** +```json +"capabilities": { + "keyphrases": [ + "weather", + "forecast", + "temperature" + ], + "languages": ["en-us"], + "descriptions": [ + "Provides current weather and forecasts for any city worldwide." + ], + "supportedLayers": ["text", "voice"] +} +``` + +--- + +### Complete agDef.json Example + +```json +{ + "manifest": { + "identification": { + "serviceUrl": "https://yourdomain.com/ov1/ag/max/run.php", + "speakerUri": "tag:yourdomain.com,2026:max", + "conversationalName": "Max", + "role": "Customer Support Specialist", + "department": "Support", + "organization": "Acme Corp", + "synopsis": "Customer support assistant for Acme Corp products." + }, + "character": { + "headShot": "max_avatar.png", + "voice": { + "vendor": "MS_EDGE", + "name": "Guy", + "uri": "Microsoft Guy Online (Natural) - English (United States)" + } + }, + "capabilities": { + "keyphrases": [ + "returns", + "warranty", + "product support", + "troubleshooting" + ], + "languages": ["en-us"], + "descriptions": [ + "Handles customer support inquiries for Acme Corp products including returns and warranties." + ], + "supportedLayers": ["text", "voice"] + } + } +} +``` + +--- + +## Customizing myAgFun.php — Your Agent Logic + +The `myAgFun.php` file contains the `agentFunctions` class, which extends the `baseAgentFunctions` class from the framework. This is where you implement your agent's conversational behavior. + +### The Three Core Methods + +For a minimalist agent, you only need to implement **one method**. All three are shown below: + +--- + +#### `inviteAction( $reason )` — Responding to an Invitation + +Called when the floor manager invites your agent to join a conversation. + +```php +public function inviteAction( $reason ) { + // $reason: [string] why you were invited (often "none" for a general invite, + // or "@sentinel" for a sentinel/background agent role) + $say = 'Hello! How can I help you today?'; + return $say; +} +``` + +Return the string you want your agent to say upon joining. Return an empty string `''` to join silently. + +--- + +#### `utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse )` — Responding to Speech + +**This is the most important method.** Called every time any participant in the conversation says something. + +```php +public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) { + // $heard — [string] what was said + // $fromUri — [string] the speakerUri of who said it + // $directedToMe — [bool] true if explicitly addressed to your agent, + // OR if your conversationalName appears in the utterance + // $directedToSomeoneElse — [bool] true if explicitly addressed to a different agent + + if ( $directedToMe ) { + $say = 'You said: ' . $heard . '. Let me help with that!'; + } elseif ( $directedToSomeoneElse ) { + $say = ''; // Stay silent — it wasn't meant for you + } else { + // Undirected utterance — broadcast to everyone + $say = 'I heard: ' . $heard; + } + + return $say; // Return '' to stay silent +} +``` + +> **Tip:** For a polite, well-behaved agent, respond only when `$directedToMe` is true and stay silent when `$directedToSomeoneElse` is true. + +> **Name detection is automatic:** If your agent's `conversationalName` (from `agDef.json`) appears anywhere in the utterance, `$directedToMe` is automatically set to `true` by the framework. + +--- + +#### `startUpAction()` and `wrapUpAction()` — Persistent State + +Use these to manage state that persists across conversation turns. The `$this->persistObject` array is automatically saved to disk at the end of each turn and restored at the start of the next. + +```php +public function startUpAction() { + parent::startUpAction(); // REQUIRED — restores $this->persistObject + + if ( $this->persistObject == null ) { // First turn — initialize your state + $this->persistObject = [ + 'exchanges' => [], + 'userName' => null, + ]; + } + // $this->persistObject is now available for the rest of this turn +} + +public function wrapUpAction() { + // Do any cleanup here before state is saved + parent::wrapUpAction(); // REQUIRED — saves $this->persistObject +} +``` + +You can store anything JSON-serializable in `$this->persistObject`. + +--- + +### Minimal Agent Template + +The absolute minimum implementation for a functional agent: + +```php +persistObject == null ) { + $this->persistObject = ['exchanges' => []]; + } + } + + public function wrapUpAction() { + parent::wrapUpAction(); + } + + public function inviteAction( $reason ) { + return 'Hello! I am ready to assist.'; + } + + public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) { + if ( !$directedToMe ) return ''; // Stay silent unless addressed + + // ---- YOUR LOGIC GOES HERE ---- + $say = 'You said: ' . $heard; + // ---- END YOUR LOGIC ---------- + + $this->persistObject['exchanges'][] = [ + 'heard' => $heard, + 'said' => $say + ]; + return $say; + } +} +?> +``` + +--- + +## The Parrot Example + +The included `myAgFun.php` implements a **Parrot** agent — a simple demonstration agent that repeats back what it hears. It is the "Hello World" of OFP agents. + +### Parrot Behavior + +| Situation | Parrot's Response | +|-----------|-------------------| +| Invited to conversation | *"Thankyou for the invitation. got any crackers?"* | +| Utterance directed to Parrot | *"Thanks for referencing me! I heard you say [X]. Polly wants a cracker!"* | +| Utterance directed to someone else | *(silent)* | +| Undirected utterance | *"I still need a cracker, but I heard [X] and I am not sure if it was meant for me."* | + +The `Parrot/` directory contains example output files from a real test conversation: + +- **`Parrot_test_convo_id.json`** — The persistent state file, showing the exchanges the Parrot tracked across the conversation. +- **`Parrot_test_convo_id.log`** — The conversation log, showing timestamped events from the Parrot's perspective. + +These files illustrate what the framework automatically creates in your persistent storage directory during a live conversation. + +--- + +## Testing with Postman + +[Postman](https://www.postman.com/) is a convenient tool for sending OFP messages to your agent endpoint without needing a full floor manager. + +### Basic Test Request + +- **Method:** `POST` +- **URL:** `https://yourdomain.com/ov1/ag/youragentname/run.php` +- **Headers:** `Content-Type: application/json` +- **Body (raw JSON):** + +```json +{ + "openFloor": { + "conversation": { + "id": "test_convo_001" + }, + "sender": { + "speakerUri": "ofpSocketClient", + "serviceUrl": "https://testclient.example.com" + }, + "events": [ + { + "eventType": "utterance", + "parameters": { + "dialogEvent": { + "features": { + "text": { + "tokens": [ + { "value": "Hello Parrot, how are you?" } + ] + } + } + } + } + } + ] + } +} +``` + +### Testing the Manifest + +To request your agent's manifest, send a `getManifests` event directed to your agent: + +```json +{ + "openFloor": { + "conversation": { + "id": "test_convo_001" + }, + "sender": { + "speakerUri": "ofpSocketClient", + "serviceUrl": "https://testclient.example.com" + }, + "events": [ + { + "eventType": "getManifests", + "to": { + "speakerUri": "Parrot_beaconforge" + } + } + ] + } +} +``` + +### Testing an Invite + +```json +{ + "openFloor": { + "conversation": { + "id": "test_convo_001" + }, + "sender": { + "speakerUri": "floorManager", + "serviceUrl": "https://floormgr.example.com" + }, + "events": [ + { + "eventType": "invite", + "to": { + "speakerUri": "Parrot_beaconforge" + } + } + ] + } +} +``` + +For a full Postman walkthrough, see the [using_postman.md](../using_postman.md) guide if available, or visit the [open-voice-sandbox](https://github.com/open-voice-interoperability/open-voice-sandbox) project for a complete OFP client. + +--- + +## How It Works (Under the Hood) + +Here is the request lifecycle for a single OFP message: + +``` +HTTP POST → run.php + │ + ▼ + simpleProcessOFP() in beaconforgeV3.php + │ + ├─ Loads agDef.json and instantiates agentFunctions (myAgFun.php) + ├─ Restores persistent state from disk (startUpAction) + ├─ Checks if the message was sent by this agent (ignores if so) + │ + ├─ For each OFP event: + │ ├─ "invite" → calls inviteAction(), sends acceptInvite + utterance + │ ├─ "utterance" → calls utteranceAction(), sends utterance response + │ └─ "getManifests" → reads agDef.json, sends manifest reply + │ + ├─ Saves persistent state to disk (wrapUpAction) + └─ Returns OFP JSON response +``` + +The framework handles all OFP envelope formatting, sender/recipient routing, manifest serialization, and HTTP response construction. Your `myAgFun.php` code only ever deals with plain PHP strings and arrays. + +--- + +## Further Reading + +- [Open Floor Protocol Specifications](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications) +- [Assistant Manifest Spec 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/AssistantManifest/1.0.1) +- [Conversation Envelope Spec 1.0.1](https://github.com/open-voice-interoperability/openfloor-docs/tree/main/specifications/ConversationEnvelope/1.0.1) +- [Open Voice Sandbox (OFP client)](https://github.com/open-voice-interoperability/open-voice-sandbox) +- [Agentic AI Research Paper](https://arxiv.org/abs/2407.19438) +- [Multi-party Conversation Research Paper](https://arxiv.org/abs/2411.05828) + +--- + +*Part of the [BeaconForge](https://github.com/open-voice-interoperability/beaconforge) project — Open Voice Interoperability Initiative, LF AI & Data Foundation.* +*Author: Emmett Coin, 2026* diff --git a/V3/bfPHP/agDef.json b/V3/bfPHP/agDef.json new file mode 100644 index 0000000..1d29ae0 --- /dev/null +++ b/V3/bfPHP/agDef.json @@ -0,0 +1,37 @@ +{ "manifest": { + "identification": { + "serviceUrl": "http://localhost/public/ov1/ag/beaconforge/run.php", + "speakerUri": "Parrot_beaconforge", + "conversationalName": "Parrot", + "role": "speech repeater", + "department": "Birdbraini", + "organization": "Aviary Network", + "synopsis": "Just dumb repeats of what it hears." + }, + "character": { + "headShot": "aParrot.png", + "voice": { + "vendor": "MS_EDGE", + "name": "Zira", + "uri": "Microsoft Zira - English (United States)" + } + }, + "capabilities": { + "keyphrases": [ + "repeat", + "say what I hear", + "polly wants a cracker" + ], + "languages": [ + "en-us" + ], + "descriptions": [ + "Just to test if the agent works" + ], + "supportedLayers": [ + "text", + "voice" + ] + } + } +} diff --git a/V3/bfPHP/beaconforgeV3.php b/V3/bfPHP/beaconforgeV3.php new file mode 100644 index 0000000..7aaf25a --- /dev/null +++ b/V3/bfPHP/beaconforgeV3.php @@ -0,0 +1,639 @@ +setConvoId( $inputData['openFloor']['conversation']['id'] ); + + $cleanName = preg_replace('/[^a-zA-Z0-9_]/', '_', $agFun->convoName); + //$myDataDir = $pathForPersistantStorage . $cleanName . '/'; + $myDataDir = $pathForPersistantStorage . $cleanName; + if ( !is_dir( $myDataDir )) {// Directory does not exist, so try to create it + if ( !mkdir($myDataDir, 0755, true)) { + echo "Directory for persistant storage could NOT be created"; + } + } + + $agFun->usePersist( $myDataDir ); + $agFun->useLog( $myDataDir ); + $agFun->startUpAction(); + $oms = new OFPMessages( $inputData, $agFun ); + $agFun->shareOFPmsg( $oms ); + + $mySpeakerUri = $agFun->getSpeakerUri(); + $myURL = $agFun->getURL(); + $myAltURL = $agFun->getAltURL(); + $reason = 'none'; + $eventTo; + $senderSpeakerUri = ''; + $senderServicerUrl = ''; + $sentByMe = false; + + if (isset($inputData['openFloor']['sender'])) { // who sent this? + if (isset($inputData['openFloor']['sender']['speakerUri'])) { + $senderSpeakerUri = $inputData['openFloor']['sender']['speakerUri']; + } + if (isset($inputData['openFloor']['sender']['serviceUrl'])) { + $senderServicerUrl = $inputData['openFloor']['sender']['serviceUrl']; + } + if (($mySpeakerUri === $senderSpeakerUri) || ($myURL === $senderServicerUrl ) || ($myAltURL === $senderServicerUrl ) ){ + $sentByMe = true; // this agent sent this OFP so ignore it. + } + } + + if( !$sentByMe){ + if (isset($inputData['openFloor']['events'])) { // is this the expected OFP? + foreach ($inputData['openFloor']['events'] as $event) { // Loop to find "invite" + if ($event['eventType'] === 'invite') { + $eventTo = $event['to']; + + $url = getUrs( $eventTo, 'serviceUrl' ); + $uri = getUrs( $eventTo, 'speakerUri' ); + $alturl = getUrs( $eventTo, 'ALTserviceUrl' ); + + if ( ($mySpeakerUri === $uri) || ($myURL === $url) || ($myAltURL === $url) ){ + if( $url === $myAltURL){ + $agFun->changeURL( $myAltURL ); // switch to the ALT URL if that is where the message is being directed + $note = "This message was directed to my ALT URL: $myAltURL"; + $agFun->addConvoPersistNote( $note ); + } + $say = $agFun->inviteAction( $reason ); + $oms->acceptInvite( true ); + $note = "I received an invite with reason [ $reason ] from $senderSpeakerUri. I accepted the invite and said: $say"; + $agFun->addConvoPersistNote( $note ); + $oms->buildUttReply( $say ); + } + } + } + foreach ($inputData['openFloor']['events'] as $event) { + $directedToMe = false; + $directedToSomeoneElse = false; + if (isset($event['to'])) { + $eventTo = $event['to']; + $url = getUrs( $eventTo, 'serviceUrl' ); + $uri = getUrs( $eventTo, 'speakerUri' ); + $alturl = getUrs( $eventTo, 'ALTserviceUrl' ); + $directedToMe = ( $mySpeakerUri === $uri || $myURL === $url || $myAltURL === $url ); + if(!$directedToMe){ + $directedToSomeoneElse = true; + } + if( $url === $myAltURL){ + $agFun->changeURL( $myAltURL ); // switch to the ALT URL if that is where the message is being directed + $note = "This message was directed to my ALT URL: $myAltURL"; + $agFun->addConvoPersistNote( $note ); + } + } + if ($event['eventType'] === 'utterance') { + if( isset($event['parameters']['dialogEvent']['features']['text']['tokens'][0]['value']) ){ + $heard = $event['parameters']['dialogEvent']['features']['text']['tokens'][0]['value']; + } + $note = "I heard: $heard from $senderSpeakerUri. toMe? " . ($directedToMe ? "Yes" : "No"); + $agFun->addConvoPersistNote( $note ); +///* + if (containsStr($heard, $agFun->getConversationalName())) { // my name is in the sentence so it is directed to me even if not explicitly + $directedToMe = true; + $directedToSomeoneElse = false; + } +//*/ + $say = $agFun->utteranceAction( $heard, $senderSpeakerUri, $directedToMe, $directedToSomeoneElse ); + $oms->buildUttReply( $say ); + + $note = "I said: $say"; + $agFun->addConvoPersistNote( $note ); + + }elseif ( ($event['eventType'] === 'getManifests') && $directedToMe ) { + $manifest = $agFun->getManifestArray(); + $oms->buildManifestReply( $manifest); + $oms->buildUttReply( "Manifest sent." ); + $note = "I sent the manifest and said: Manifest sent."; + $agFun->addConvoPersistNote( $note ); + + } + } + } + }else{ + $oms->buildAcknowledge(); // just acknowledge that you got the message (http politeness) + } + $agFun->wrapUpAction(); + return $oms->loadForReturn( $agFun); +} + +function getUrs( $evTo, $type ){ + if (isset($evTo[$type])) { + return $evTo[$type]; + } +} + +function containsStr(string $sentence, string $searchFor): bool { + // stripos() returns the position (an integer, 0 if at the start) or false if not found. + // We use the strict comparison operator (!== false) to ensure a correct boolean result, + // as 0 is a valid position but is "falsy" in loose comparisons. + if (stripos($sentence, $searchFor) !== false) { + return true; + } else { + return false; + } +} + +class OFPMessages { + private $eventArray; + private $retOFP; + private $mySpeakerUri; + private $myURL; + private $replyTo; + + public function __construct( $inputOFP, $agFunc ) { + $this->eventArray = []; + $this->replyTo = $inputOFP['openFloor']['sender']; + $this->retOFP = $inputOFP; + + // these are ignored on return so just delete them for neatness + if (isset($this->retOFP['openFloor']['conversation']['conversants'])) { //remove conversants + unset($this->retOFP['openFloor']['conversation']['conversants']); + } + if (isset($this->retOFP['openFloor']['conversation']['assignedFloorRoles'])) { //remove assignedFloorRoles + unset($this->retOFP['openFloor']['conversation']['assignedFloorRoles']); + } + if (isset($this->retOFP['openFloor']['conversation']['floorGranted'])) { //remove floorGranted + unset($this->retOFP['openFloor']['conversation']['floorGranted']); + } + + //$this->retOFP['openFloor']['sender']['serviceUrl'] = $agFunc->getURL(); + //$this->retOFP['openFloor']['sender']['speakerUri'] = $agFunc->getSpeakerUri(); + $this->mySpeakerUri = $agFunc->getSpeakerUri(); + $this->myURL = $agFunc->getURL(); + } + + public function loadForReturn( $agFunc ){ + $this->retOFP['openFloor']['sender']['serviceUrl'] = $agFunc->getURL(); + $this->retOFP['openFloor']['sender']['speakerUri'] = $agFunc->getSpeakerUri(); + + if ( empty( $this->eventArray ) ) { + //$this->buildAcknowledge(); + } + $this->retOFP['openFloor']['events'] = $this->eventArray; // add the new events + $currentDateTime = new DateTime(); + $this->retOFP['openFloor']['conversation']['startTime'] = $currentDateTime->format('m-d-Y_H:i:s'); + return $this->retOFP; + } + + public function buildUttReply( $whatToSay ){ + if( $whatToSay != '' ){ // otherwise ignore it + $this->buildReply( 'utterance', $whatToSay ); + } + } + + public function buildReply( $type, $whatToSay ){ + if( strlen($whatToSay) > 0 ){ // skip if nothing + $newEvent = [ + 'to' => $this->replyTo, + 'eventType' => $type, + 'parameters' => [ + 'dialogEvent' =>[ + 'speakerUri'=> $this->mySpeakerUri, + 'features' => [ + 'text' => [ + 'mimeType' => 'text/plain', + 'tokens' => [ + ['value' => $whatToSay ] + ] + ] + ] + ] + ] + ]; + $this->eventArray[] = $newEvent; + } + } + + public function buildManifestReply( $theManifest ){ + $newEvent = [ + 'to' => $this->replyTo, + 'eventType' => 'publishManifests', + 'parameters' => [ + 'servicingManifests' => [ + $theManifest + ], + 'discoveryManifests' => [ + $theManifest + ] + ] + ]; + $this->eventArray[] = $newEvent; + } + + public function buildAcknowledge(){ + // think about who should get this convener? floor? all? + $newEvent = [ + 'to' => $this->replyTo, + 'eventType' => 'acknowledge', + ]; + $this->eventArray[] = $newEvent; + } + + public function acceptInvite( $bool ){ + // think about who should get this convener? floor? all? + $evType = 'declineInvite'; + if( $bool ) { + $evType = 'acceptInvite'; + } + $newEvent = [ + 'to' => $this->replyTo, + 'eventType' => $evType, + ]; + $this->eventArray[] = $newEvent; + } + + public function addRawEvent( $someEvent ){ + // You are responsible for building a valid event + $this->eventArray[] = $someEvent; + } +} + +class baseAgentFunctions { + protected $agent; + public $convoName; + private $URL; + private $AltURL = ''; + private $speakerUri; + private $manifest; + protected $persistFileName = ''; + protected $persistObject = null; + protected $usePersistObject = false; + protected $OFPTool = null; + private $convoId = ''; + private $agentJSONFile = ''; + protected $relativePathForFiles; + + protected $thisTurnData = []; + protected $logObject = null; + protected $logFileName = ''; + protected $useLogObject = false; + + public function startUpAction() { + // some code to initialize this. + // e.g. read persistant data, or set up llm etc + if (!file_exists($this->logFileName) && $this->useLogObject){ //start fresh + $this->logObject = [ + 'convoLog' => [], + ]; + $this->saveLogObject( $this->logObject ); + }else{ + if( $this->useLogObject ){ + $this->logObject = $this->getLogObject(); + } + } + if (!file_exists($this->persistFileName) && $this->usePersistObject){ //start fresh + $this->persistObject = null; + $this->savePersistObject( $this->persistObject ); + }else{ + if( $this->usePersistObject ){ + $this->persistObject = $this->getPersistObject(); + } + } + } + + public function wrapUpAction() { + // some code to finalize this. + // e.g. save persistant data or do final llm post + // REMEMBER: persistantLogObject is JSON stringified then saved + if( $this->usePersistObject ){ + $this->savePersistObject( $this->persistObject ); //save for next turn + } + if( $this->useLogObject ){ // add this turn's data to the log and save it for next turn + $this->saveLogObject( $this->logObject ); + } + } + + public function inviteAction( $reason ) { + $say = 'Hi, how can I help?'; + // This is what you say upon your invitation to the conversation + return $say; + } + + public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) { + $say = 'I heard: ' . $heard; + // This is a public message sent to everyone on the conversation + // Do your thing with $heard and create a $say + // Use the $directedToMe boolean to behave differently when not directed to you + return $say; + } + + public function getManifestArray() { + $manifest = $this->agent['manifest']; + return $manifest; + } + + + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // The above functions will be overloaded in your EXTENDED class + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + public function __construct( $fileName ) { + $this->agentJSONFile = $fileName; + $this->agent = readJSONFromFile( $fileName ); + $this->manifest = $this->agent['manifest']; + $this->URL = $this->agent['manifest']['identification']['serviceUrl']; + //$this->agentName = $this->agent['manifest']['identification']['conversationalName']; + if( isset($this->agent['manifest']['identification']['ALTserviceUrl']) ){ + $this->AltURL = $this->agent['manifest']['identification']['ALTserviceUrl']; + } + $this->convoName = $this->agent['manifest']['identification']['conversationalName']; + $this->speakerUri = $this->agent['manifest']['identification']['speakerUri']; + } + + public function shareOFPmsg( $oms ) { + $this->OFPTool = $oms; + } + + public function useLog( $relativePath ){ + // $relativePath [str e.g. '../../private'] + $this->relativePathForFiles = $relativePath; + $this->useLogObject = true; + $cleanName = preg_replace('/[^a-zA-Z0-9_]/', '_', $this->convoName); + $cleanName .= '_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $this->convoId); + $this->logFileName = $relativePath . '/' . $cleanName .'.log'; + } + + public function getLogObject(){ + // this will return null if not there so you must build one + $this->logObject = null; + if( $this->useLogObject ){ + $this->logObject = readJSONFromFile( $this->logFileName ); + } + return $this->logObject; + } + + public function saveLogObject( $pObject ){ + // this will save the object to a file for the next turn + if( $this->useLogObject && ($pObject != null) ){ + $strJSON = json_encode( $pObject ); + file_put_contents( $this->logFileName, $strJSON ); + }; + } + + public function usePersist( $relativePath ){ + // $relativePath [str e.g. '../../private'] + $this->relativePathForFiles = $relativePath; + $this->usePersistObject = true; + $cleanName = preg_replace('/[^a-zA-Z0-9_]/', '_', $this->convoName); + $cleanName .= '_' . preg_replace('/[^a-zA-Z0-9_]/', '_', $this->convoId); + $this->persistFileName = $relativePath . '/' . $cleanName .'.json'; + } + + public function getPersistObject(){ + // this will return null if not there so you must build one + $this->persistObject = null; + if( $this->usePersistObject ){ + $this->persistObject = readJSONFromFile( $this->persistFileName ); + } + return $this->persistObject; + } + + public function savePersistObject( $pObject ){ + // this will save the object to a file for the next turn + if( $this->usePersistObject && ($pObject != null) ){ + $strJSON = json_encode( $pObject ); + file_put_contents( $this->persistFileName, $strJSON ); + }; + } + + //public function getAgentName() { + // return $this->agentName; + //} + + public function getURL() { + return $this->URL; + } + + public function changeURL( $newURL) { + $this->URL = $newURL; + return $this->URL; + } + + public function getAltURL(){ + return $this->AltURL; + } + + public function getSpeakerUri() { + return $this->speakerUri; + } + + public function getConversationalName() { + return $this->convoName; + } + + public function setConvoId( $someId ) { + $this->convoId = $someId; + } + + public function addConvoPersistNote( $someNote ) { + // set time and note into this turn's data to be added to the persistant object at the end of the turn + $this->thisTurnData[] = [ + 'time' => date('m-d-Y_H:i:s'), + 'note' => $someNote + ]; + $this->logObject['convoLog'][] = $this->thisTurnData; + $this->saveLogObject( $this->logObject ); + } +} + +// Define the file path +//$filePath = 'example.txt'; +$errors = []; + +function writeToFile($filePath, $content) { + // Open the file for writing; creates the file if it doesn't exist + $fileHandle = fopen($filePath, 'w'); + if ($fileHandle) { + fwrite($fileHandle, $content); // Write content to file + fclose($fileHandle); + } else { + $errors[] = "Failed to open the file for writing.\n"; + } +} + +function readFromFile($filePath) { + if (file_exists($filePath)) { // file exists? + $fileHandle = fopen($filePath, 'r'); + if ($fileHandle) { + $content = fread($fileHandle, filesize($filePath)); + fclose($fileHandle); + return $content; + } else { + $errors[] = "Failed to open the file for reading."; + } + } else { + $errors[] = "File does not exist."; + } +} + +function readJSONFromFile($filePath) { + //echo 'filePath = ' . $filePath . PHP_EOL; //ejcdbg + $data = null; + if (file_exists($filePath)) { // file exists? + $fileHandle = fopen($filePath, 'r'); + if ($fileHandle) { + $content = fread($fileHandle, filesize($filePath)); + fclose($fileHandle); + $data = json_decode($content, true); // Decode JSON to PHP variable + if (json_last_error() !== JSON_ERROR_NONE) { // Good decode? + $errors[] = "JSON Decode Error"; + } + } else { + $errors[] = "Failed to open the file for reading." . PHP_EOL; + } + } else { + $errors[] = "File does not exist." . PHP_EOL; + } + return $data; +} + +class FileIO { + private $path; + + public function setPath($path) { + $this->path = $path; + } + + public function ejReadFile($fileName, $type, $isArray = true) { + $filePath = $this->path . $fileName; + if (!file_exists($filePath)) { + throw new Exception("File not found: " . $filePath); + } + $fileContents = file_get_contents($filePath); + if ($type === 'json') { + return json_decode($fileContents, $isArray); + } + return $fileContents; + } +} + + +class SimpleNLP { + private $myConcepts = null; + + public function __construct( $fileName ) { + $pData = file_get_contents( $fileName ); + if( strlen($pData) > 40 ){ + $this->myConcepts = json_decode($pData, true); // ready to use + if (json_last_error() !== JSON_ERROR_NONE) { + echo 'json_decode error: ' . json_last_error_msg(); + } + }else{ // not a good file so rebuild the LLM agent + echo 'no file for intents was found'; + } + + } + + public function simpleIntentFromText($inputMessage) { + $matchedConcepts = []; + $message = ' ' . preg_replace('/[^\w\s]/', ' ', $inputMessage) . ' '; + $message = strtolower($message); + $words = ''; + + if ($this->myConcepts) { + foreach ($this->myConcepts as $concept) { + $matchedWords = array_filter($concept['examples'], function($word) use ($message) { + $spacedWord = ' ' . strtolower($word) . ' '; + return strpos($message, $spacedWord) !== false; + }); + + if (!empty($matchedWords)) { + foreach ($matchedWords as $value) { // only way to stop from getting the index also + $words = $value; + } + $matchedConcepts[] = [ + "concept" => $concept['name'], + "matchedWords" => $words + ]; + } + } + } + return $matchedConcepts; + } + + public function simpleIntent($conceptJSON) { + $concept = ""; + $intent = [ + "return" => false, + "assistantName" => "", + "repeatLastUtt" => false, + "manifest" => false + ]; + + if ($conceptJSON) { + foreach ($conceptJSON as $conceptData) { + $concept = $conceptData['concept']; + if ($concept === "return") { + $intent["return"] = true; + } else if ($concept === "delegate") { + $intent["redirect"] = $concept; + } else if ($concept === "assistantName") { + $intent["assistantName"] = $conceptData['matchedWords']; + } else if ($concept === "repeatLastUtt") { + $intent["repeatLastUtt"] = true; + } else if ($concept === "manifest") { + $intent["manifest"] = true; + } + } + } + return $intent; + } + + public function loadIntents( $type, $whatToSay ){ + $myConcepts = [ + [ + "name" => "bye", + "examples" => [ + "goodbye", "farewell", "bye", "see you", "have a good day", "goodnight", "talk to you later", "catch you later", "see you later", "take care", "I have to go", "I'm leaving", "until next time" + ] + ], + [ + "name" => "maybe", + "examples" => [ + "maybe", "perhaps", "not sure", "I don't know", "possibly", "could be", "might be", "it's a possibility", "I guess so", "I suppose so" + ] + ], + [ + "name" => "yes", + "examples" => [ + "yes", "yeah", "okay", "why not", "sure", "alright", "of course", "definitely" + ] + ], + [ + "name" => "no", + "examples" => [ + "no", "not now", "never", "don't want to", "nope", "not really", "don't think so" + ] + ], + [ + "name" => "stillThere", + "examples" => [ + "you still there", "you there", "are you listening", "can you here me" + ] + ], + [ + "name" => "politeness", + "examples" => [ + "thank you", "excuse me", "please", "sorry", "do you mind", "pardon me", "welcome", "excuse me", "my apologies", "no problem", "don't mention it", "that's alright" + ] + ], + [ + "name" => "greeting", + "examples" => [ + "hello", "hi", "hey", "how is it going", "good morning", "good afternoon", "good evening", "what's up", "howdy", "greetings", "salutations", "nice to meet you", "pleased to meet you" + ] + ] + ]; + } +} +?> \ No newline at end of file diff --git a/V3/bfPHP/myAgFun.php b/V3/bfPHP/myAgFun.php new file mode 100644 index 0000000..d1d46cd --- /dev/null +++ b/V3/bfPHP/myAgFun.php @@ -0,0 +1,55 @@ +persistObject == null ){ // first time, so init it + $this->persistObject = [ + 'exchanges' => [] + ]; + } + } + + public function wrapUpAction() { + // Do whatever needs doing before leaving + // REMEMBER: persistObject will be stringified and saved + parent::wrapUpAction(); // You MUST call this to save the persist object + } + + public function inviteAction( $reason ) { + $say = 'Thankyou for the invitation. got any crackers?'; + // Note: if $reason=="@sentinal" then modify your behavior accordingly + return $say; + } + + public function utteranceAction( $heard, $fromUri, $directedToMe, $directedToSomeoneElse ) { + // To be polite you may want to respond ONLY if $directedToMe is true. + // You may want to avoid responding if it is meant for someone else + if( strlen( $heard ) <1 ){ + $heard = "nothing"; + } + if( $directedToMe ){ // it was meant for me, so respond regardless. + $say = 'Thanks for referencing me! I heard you say ' . $heard . '. Polly wants a cracker!'; + }else if( $directedToSomeoneElse ){// You may want to avoid responding here + $say = ''; + }else{ // Must be directed to everyone + $say = 'I still need a cracker, but I heard '. $heard . ' and I am not sure if it was meant for me.'; + } + // keep track of what you heard in the persistant object + $this->persistObject['exchanges'][] = ["utt" => $heard, "from" => $fromUri, "toMe" => $directedToMe, "toSomeoneElse" => $directedToSomeoneElse, "said" => $say]; + + return $say; + } +} +?> \ No newline at end of file diff --git a/V3/bfPHP/run.php b/V3/bfPHP/run.php new file mode 100644 index 0000000..75c4c3d --- /dev/null +++ b/V3/bfPHP/run.php @@ -0,0 +1,38 @@ + json_last_error()]); + exit; +} + +echo json_encode( simpleProcessOFP($inputOpenfloor, $agentDefinitionJSON ) ); // return OFP +?> \ No newline at end of file From 131cc5fa8d094dc81676d828de6ac5694826f1d8 Mon Sep 17 00:00:00 2001 From: industrialpoet Date: Tue, 24 Mar 2026 14:09:00 -0400 Subject: [PATCH 3/3] new version Signed-off-by: industrialpoet --- V3/bfPHP/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/V3/bfPHP/README.md b/V3/bfPHP/README.md index d1c3f39..253c48a 100644 --- a/V3/bfPHP/README.md +++ b/V3/bfPHP/README.md @@ -188,7 +188,7 @@ This section is **mandatory** and defines the core identity of your agent. It is | `department` | string | No | The area or department within the organization. | | `role` | string | No | The agent's job title or role (e.g., "Weather Specialist", "Customer Support"). | | `openFloorRoles` | dict | No | OFP roles this agent can perform. E.g., `{ "convener": true }` or `{ "discovery": true }`. | -| `ALTserviceUrl` | URL string | No | An alternative service URL. The framework will switch to this if a message is directed to it. | + **Example:** ```json