Repository: Point72/csp-adapter-symphony

Issue:
Add documentation for setting up Symphony adapter with a real symphony instance
We should document everything needed to spin up this adapter against a real symphony instance, including some test code along the lines of replying "hello world" when you @ the bot.

Discussion:

Solution patch:
diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml
index 596e8c8..3b8fc33 100644
--- a/.github/workflows/wiki.yml
+++ b/.github/workflows/wiki.yml
@@ -23,6 +23,9 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
 
+      - name: Make home file
+        run: cp README.md docs/wiki/Home.md
+        
       - name: Upload Documentation to Wiki
         uses: Andrew-Chen-Wang/github-wiki-action@v4
         with:
diff --git a/Makefile b/Makefile
index f33d40a..d52f878 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ install:  ## install to site-packages
 test: ## run the python unit tests
 	python -m pytest -v csp_adapter_symphony/tests --cov=csp_adapter_symphony --cov-report xml --cov-report term-missing
 
-test: tests
+tests: test
 
 ###########
 # Linting #
diff --git a/csp_adapter_symphony/__init__.py b/csp_adapter_symphony/__init__.py
index dce9534..e084d24 100644
--- a/csp_adapter_symphony/__init__.py
+++ b/csp_adapter_symphony/__init__.py
@@ -2,3 +2,5 @@
 
 from .adapter import *
 from .adapter_config import *
+from .mention import *
+from .message import *
diff --git a/csp_adapter_symphony/adapter.py b/csp_adapter_symphony/adapter.py
index eeb94b8..507e58c 100644
--- a/csp_adapter_symphony/adapter.py
+++ b/csp_adapter_symphony/adapter.py
@@ -12,39 +12,13 @@
 from csp.impl.wiring import py_push_adapter_def
 
 from .adapter_config import SymphonyAdapterConfig, SymphonyRoomMapper
+from .message import SymphonyMessage, format_with_message_ml
 
-__all__ = ("Presence", "SymphonyAdapter", "SymphonyMessage", "mention_user", "send_symphony_message", "format_with_message_ml")
+__all__ = ("Presence", "SymphonyAdapter", "send_symphony_message")
 
 log = logging.getLogger(__file__)
 
 
-def format_with_message_ml(text, to_message_ml: bool = True) -> str:
-    """If to_message_ml, we convert to message ml by replacing special sequences of character. Else, we convert from message_ml in the same way"""
-    pairs = [
-        ("&", "&#38;"),
-        ("<", "&lt;"),
-        ("${", "&#36;{"),
-        ("#{", "&#35;{"),
-    ]
-
-    for original, msg_ml_version in pairs:
-        if to_message_ml:
-            text = text.replace(original, msg_ml_version)
-        else:
-            text = text.replace(msg_ml_version, original)
-
-    return text
-
-
-def mention_user(email_or_userid: str = ""):
-    if email_or_userid:
-        if "@" in str(email_or_userid):
-            return f'<mention email="{email_or_userid}" />'
-        else:
-            return f'<mention uid="{email_or_userid}" />'
-    return ""
-
-
 def _sync_create_data_feed(datafeed_create_url: str, header: Dict[str, str]) -> Tuple[requests.Response, str]:
     r = requests.post(
         url=datafeed_create_url,
@@ -108,17 +82,6 @@ def _get_user_mentions(payload):
         return user_mentions
 
 
-class SymphonyMessage(csp.Struct):
-    user: str
-    user_email: str  # email of the author, for mentions
-    user_id: str  # uid of the author, for mentions
-    tags: List[str]  # list of user ids in message, for mentions
-    room: str
-    msg: str
-    form_id: str
-    form_values: dict
-
-
 def _handle_event(event: dict, room_ids: set, room_mapper: SymphonyRoomMapper) -> Optional[SymphonyMessage]:
     if ("type" not in event) or ("payload" not in event):
         return None
diff --git a/csp_adapter_symphony/adapter_config.py b/csp_adapter_symphony/adapter_config.py
index d981b9a..29b3a19 100644
--- a/csp_adapter_symphony/adapter_config.py
+++ b/csp_adapter_symphony/adapter_config.py
@@ -24,9 +24,9 @@ class SymphonyAdapterConfig(BaseModel):
     session_auth_path: str = Field(descrption="Path to authenticate session, like `/sessionauth/v1/authenticate`")
     key_auth_path: str = Field(description="Path to authenticate key, like `/keyauth/v1/authenticate`")
     message_create_url: str = Field(
-        description="String path to create a message, like `https://SYMPHONY_HOST/agent/v4/stream/{{sid}}/message/create`"
+        description="Format-string path to create a message, like `https://SYMPHONY_HOST/agent/v4/stream/{{sid}}/message/create`"
     )
-    presence_url: str = Field(description="String path to create a message, like `https://SYMPHONY_HOST/pod/v2/user/presence`")
+    presence_url: str = Field(description="String path to set presence information, like `https://SYMPHONY_HOST/pod/v2/user/presence`")
     datafeed_create_url: str = Field(description="String path to create datafeed, like `https://SYMPHONY_HOST/agent/v5/datafeeds`")
     datafeed_delete_url: str = Field(
         description="Format-string path to create datafeed, like `https://SYMPHONY_HOST/agent/v5/datafeeds/{{datafeed_id}}`"
@@ -39,8 +39,10 @@ class SymphonyAdapterConfig(BaseModel):
     room_members_url: Optional[str] = Field(
         None, description="Format-string path to get room members in a room, like `https://SYMPHONY_HOST/pod/v2/room/{{room_id}}/membership/list`"
     )
-    cert_string: Union[str, FilePath] = Field(description="Pem format string of client certificate")
-    key_string: Union[str, FilePath] = Field(description="Pem format string of client private key")
+
+    cert: Union[str, FilePath] = Field(description="Pem format string of client certificate")
+    key: Union[str, FilePath] = Field(description="Pem format string of client private key")
+
     error_room: Optional[str] = Field(
         None,
         description="A room to direct error messages to, if a message fails to be sent over symphony, or if the SymphonyReaderPushAdapter crashes",
@@ -51,14 +53,14 @@ class SymphonyAdapterConfig(BaseModel):
     multiplier: float = Field(2.0, description="Multiplier between attempt delays for exponential backoff")
     max_interval_ms: int = Field(300000, description="Maximum delay between retry attempts, in milliseconds")
 
-    @field_validator("cert_string")
+    @field_validator("cert")
     def load_cert_if_file(cls, v):
         if "BEGIN CERTIFICATE" in v:
             return v
         with open(v, "r") as fp:
             return fp.read()
 
-    @field_validator("key_string")
+    @field_validator("key")
     def load_key_if_file(cls, v):
         if "BEGIN PRIVATE KEY" in v:
             return v
@@ -75,8 +77,8 @@ def header(self) -> Dict[str, str]:
             auth_host=self.auth_host,
             session_auth_path=self.session_auth_path,
             key_auth_path=self.key_auth_path,
-            cert_string=self.cert_string,
-            key_string=self.key_string,
+            cert_string=self.cert,
+            key_string=self.key,
         )
 
     def get_retry_decorator(self) -> Callable:
diff --git a/csp_adapter_symphony/examples/__init__.py b/csp_adapter_symphony/examples/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/csp_adapter_symphony/examples/hello.py b/csp_adapter_symphony/examples/hello.py
new file mode 100644
index 0000000..e69de29
diff --git a/csp_adapter_symphony/mention.py b/csp_adapter_symphony/mention.py
new file mode 100644
index 0000000..e1f3f3a
--- /dev/null
+++ b/csp_adapter_symphony/mention.py
@@ -0,0 +1,10 @@
+__all__ = ("mention_user",)
+
+
+def mention_user(email_or_userid: str = ""):
+    if email_or_userid:
+        if "@" in str(email_or_userid):
+            return f'<mention email="{email_or_userid}" />'
+        else:
+            return f'<mention uid="{email_or_userid}" />'
+    return ""
diff --git a/csp_adapter_symphony/message.py b/csp_adapter_symphony/message.py
new file mode 100644
index 0000000..4d5a8dc
--- /dev/null
+++ b/csp_adapter_symphony/message.py
@@ -0,0 +1,34 @@
+from typing import List
+
+from csp import Struct
+
+__all__ = ("SymphonyMessage", "format_with_message_ml")
+
+
+def format_with_message_ml(text, to_message_ml: bool = True) -> str:
+    """If to_message_ml, we convert to message ml by replacing special sequences of character. Else, we convert from message_ml in the same way"""
+    pairs = [
+        ("&", "&#38;"),
+        ("<", "&lt;"),
+        ("${", "&#36;{"),
+        ("#{", "&#35;{"),
+    ]
+
+    for original, msg_ml_version in pairs:
+        if to_message_ml:
+            text = text.replace(original, msg_ml_version)
+        else:
+            text = text.replace(msg_ml_version, original)
+
+    return text
+
+
+class SymphonyMessage(Struct):
+    user: str
+    user_email: str  # email of the author, for mentions
+    user_id: str  # uid of the author, for mentions
+    tags: List[str]  # list of user ids in message, for mentions
+    room: str
+    msg: str
+    form_id: str
+    form_values: dict
diff --git a/docs/wiki/API-Reference.md b/docs/wiki/API-Reference.md
index 6863c09..c60f5a7 100644
--- a/docs/wiki/API-Reference.md
+++ b/docs/wiki/API-Reference.md
@@ -1 +1,46 @@
-Coming soon!
+# Adapter Config
+
+`SymphonyAdapterConfig` requires:
+
+- `app_token`: App Token
+- `bot_token`: OAuth Bot Token
+
+An SSL context can be provided optionally in the `ssl` argument.
+
+See [Setup](Setup) for more information.
+
+# Adapter Manager
+
+`SlackAdapterManager` takes a single argument `config`, an instance of `SlackAdapterConfig`.
+It provides two methods:
+
+- `subscribe() -> ts[[SlackMessage]]`: Subscribe to messages (`SlackMessage`) from channels in which the Bot is present, and DMs
+- `publish(ts[SlackMessage])`: Publish messages (`SlackMessage`)
+
+> [!NOTE]
+>
+> `subscribe` returns a list of `SlackMessage`, but `publish` takes an individual `SlackMessage`.
+> This is for API symmetry with the [csp-adapter-symphony](https://github.com/point72/csp-adapter-symphony).
+> `csp.unroll` can be used to unroll the list of `ts[List[SlackMessage]]` into `ts[SlackMessage]`.
+
+```python
+from csp_adapter_slack import SlackAdapterConfig, SlackAdapterManager
+
+
+def graph():
+    adapter = SlackAdapterManager(
+      config=SlackAdapterConfig(
+        app_token=".app_token",
+        bot_token=".bot_token",
+      ),
+    )
+
+    csp.print("All Messages", adapter.subscribe())
+```
+
+See [Examples](Examples) for more examples.
+
+# Chat Framework
+
+`csp-chat` is a framework for writing cross-platform, command oriented chat bots.
+It will be released in 2025 with initial support for `Slack`, `Symphony`, and `Discord`.
diff --git a/docs/wiki/Examples.md b/docs/wiki/Examples.md
new file mode 100644
index 0000000..5c79bc7
--- /dev/null
+++ b/docs/wiki/Examples.md
@@ -0,0 +1,10 @@
+The below examples are available [in-source](https://github.com/Point72/csp-adapter-symphony/blob/main/csp_adapter_symphony/examples/).
+They assume the presence of `.key` and `.cert` files in the run directory.
+You will need to modify them to point at your Symphony URL.
+
+# Hello!
+
+Here is a simple example that replies "Hello!" someone says `hello` in a room or direct message to the bot.
+It is available in-source at [`csp_adapter_symphony/examples/hello.py`](https://github.com/Point72/csp-adapter-symphony/blob/main/csp_adapter_symphony/examples/hello.py).
+
+Coming Soon!
diff --git a/docs/wiki/First-Steps.md b/docs/wiki/First-Steps.md
deleted file mode 100644
index cde982c..0000000
--- a/docs/wiki/First-Steps.md
+++ /dev/null
@@ -1 +0,0 @@
-Coming Soon!
diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md
deleted file mode 100644
index dfc7ec4..0000000
--- a/docs/wiki/Home.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# csp-adapter-symphony
-
-A [csp](https://github.com/point72/csp) adapter for [symphony](https://symphony.com)
-
-## Get Started
-
-- [Learn more about csp](https://github.com/Point72/csp)
-- [Install csp-adapter-symphony](Installation) and [get started](First-Steps)
-
-> [!TIP]
-> Find relevant docs with GitHub’s search function, use `repo:Point72/csp-adapter-symphony type:wiki <search terms>` to search the documentation Wiki Pages.
-
-## Community
-
-- [Contribute](Contribute) to `csp-adapter-symphony` and help improve the project
-
-## License
-
-`csp-adapter-symphony` is licensed under the Apache 2.0 license. See the [LICENSE](https://github.com/Point72/csp-adapter-symphony/blob/main/LICENSE) file for details.
diff --git a/docs/wiki/Setup.md b/docs/wiki/Setup.md
new file mode 100644
index 0000000..77f9a45
--- /dev/null
+++ b/docs/wiki/Setup.md
@@ -0,0 +1,84 @@
+This guide will help you setup a new Symphony application.
+
+> [!TIP]
+> Find relevant docs with GitHub’s search function, use `repo:Point72/csp-adapter-symphony type:wiki <search terms>` to search the documentation Wiki Pages.
+
+# Symphony Configuration
+
+Follow [the documentation for creating a Certificate Authentication Workflow-based bot](https://docs.developers.symphony.com/bots/authentication/certificate-authentication).
+
+Take note of:
+
+- The Bot's name
+- The Bot's Key and Certificate
+
+# Managing Key/Certificate
+
+You should have an `key` and a `cert` from the above steps.
+
+These can be configured directly on the `SymphonyAdapterConfig`:
+
+```python
+from csp_adapter_symphony import SymphonyAdapterConfig
+
+config = SymphonyAdapterConfig(key="BEGIN PRIVATE KEY...", cert="BEGIN CERTIFICATE...")
+```
+
+Alternatively, these could be stored in local files and the configuration will read them:
+
+**.gitignore**
+
+```raw
+.key
+.cert
+```
+
+**.key**
+
+```raw
+BEGIN PRIVATE KEY
+...
+```
+
+**.cert**
+
+```raw
+BEGIN CERTIFICATE
+...
+```
+
+```python
+from csp_adapter_symphony import SymphonyAdapterConfig
+
+config = SymphonyAdapterConfig(key=".key", cert=".cert")
+```
+
+# Symphony Routes
+
+Symphony is hosted per-deployment.
+As such, it can be can be configured in a variety of ways at a variety of URLs.
+
+`SymphonyAdapterConfig` allows for configuration of all the required routes via arguments. Note that some of them are format strings, and thus expect Python format string style placeholders.
+
+- `auth_host`: Authentication host, like `company-api.symphony.com`
+- `session_auth_path`: Path to authenticate session, like `/sessionauth/v1/authenticate`
+- `key_auth_path`: Path to authenticate key, like `/keyauth/v1/authenticate`
+- `message_create_url`: Format-string path to create a message, like `https://SYMPHONY_HOST/agent/v4/stream/{{sid}}/message/create`
+- `presence_url`: String path to set presence information, like `https://SYMPHONY_HOST/pod/v2/user/presence`
+- `datafeed_create_url`: String path to create datafeed, like `https://SYMPHONY_HOST/agent/v5/datafeeds`
+- `datafeed_delete_url`: Format-string path to create datafeed, like `https://SYMPHONY_HOST/agent/v5/datafeeds/{{datafeed_id}}`
+- `datafeed_read_url`: Format-string path to create datafeed, like `https://SYMPHONY_HOST/agent/v5/datafeeds/{{datafeed_id}}/read`
+- `room_search_url`: Format-string path to create datafeed, like `https://SYMPHONY_HOST/pod/v3/room/search`
+- `room_info_url`: Format-string path to create datafeed, like `https://SYMPHONY_HOST/pod/v3/room/{{room_id}}/info`
+- `room_members_url`: (Optional) Format-string path to get room members in a room, like `https://SYMPHONY_HOST/pod/v2/room/{{room_id}}/membership/list`
+
+# Extra Configuration
+
+The `SymphonyAdapterConfig` has a few extra features that can be adjusted as needed:
+
+- `error_room`: (Optional) A room to direct error messages to, if a message fails to be sent over symphony, or if the adapter crashes
+- `inform_client`: Whether to inform the intended recipient of a failed message that the message failed
+- `max_attempts`: Max attempts for datafeed and message post requests before raising exception. If -1, no maximum
+- `initial_interval_ms`: Initial interval to wait between attempts, in milliseconds
+- `multiplier`: Multiplier between attempt delays for exponential backoff
+- `max_interval_ms`: maximum delay between retry attempts, in milliseconds
diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md
index 4a48398..129b3e7 100644
--- a/docs/wiki/_Sidebar.md
+++ b/docs/wiki/_Sidebar.md
@@ -11,11 +11,9 @@ Notes for editors:
 **Get Started**
 
 - [Installation](Installation)
-- [First steps](First-Steps)
-
-**References**
-
+- [Setup](Setup)
 - [API Reference](API-Reference)
+- [Examples](Examples)
 
 **Developer Guide**
 
diff --git a/pyproject.toml b/pyproject.toml
index bf8502b..1598bee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -113,7 +113,6 @@ packages = ["csp_adapter_symphony"]
 exclude = [
     "/.github",
     "/.gitignore",
-    "/pyproject.toml",
     "/docs",
 ]
 


Test patch:
diff --git a/csp_adapter_symphony/tests/test_adapter.py b/csp_adapter_symphony/tests/test_adapter.py
index 6913924..9dbdd7f 100644
--- a/csp_adapter_symphony/tests/test_adapter.py
+++ b/csp_adapter_symphony/tests/test_adapter.py
@@ -263,8 +263,8 @@ def post_request(url, headers, json=None):
                 datafeed_read_url="https://symphony.host/agent/v5/datafeeds/{{datafeed_id}}/read",
                 room_search_url="https://symphony.host/pod/v3/room/search",
                 room_info_url="https://symphony.host/pod/v3/room/{{room_id}}/info",
-                cert_string="BEGIN CERTIFICATE:my_cert_string",  # hack to bypass file opening
-                key_string="BEGIN PRIVATE KEY:my_key_string",  # hack to bypass file opening
+                cert="BEGIN CERTIFICATE:my_cert_string",  # hack to bypass file opening
+                key="BEGIN PRIVATE KEY:my_key_string",  # hack to bypass file opening
                 error_room=None if not inform_client else "another sample room",
                 inform_client=inform_client,
             )