Skip to content

feat: Added support for Parlant Framework#66

Merged
sawradip merged 4 commits into
mainfrom
rad/parlant
Sep 8, 2025
Merged

feat: Added support for Parlant Framework#66
sawradip merged 4 commits into
mainfrom
rad/parlant

Conversation

@RadeenXALNW

@RadeenXALNW RadeenXALNW commented Sep 8, 2025

Copy link
Copy Markdown
Collaborator

Added support of parlant framework tested in python, rust and go and capable of both streaming and non-streaming support

Summary by CodeRabbit

  • New Features
    • Added Parlant framework support for creating and running Parlant-based agents.
    • Introduced a default Parlant template with calculator, time, and weather tools and guideline-driven behavior.
    • Provided simple and streaming chat entrypoints via configuration.
  • Documentation
    • Added a Quick Start guide for the Parlant template.
  • Tests
    • Added end-to-end template test and client test scripts in Python and Rust for simple and streaming flows.
  • Chores
    • Updated ignore rules for parlant-data and added initial empty cache files.

@coderabbitai

coderabbitai Bot commented Sep 8, 2025

Copy link
Copy Markdown

Walkthrough

Adds Parlant framework support: new ParlantExecutor and registry wiring; introduces a Parlant template (agent implementation, config, requirements, README); adds data cache files and .gitignore entries; provides Python and Rust test scripts for simple and streaming chat; updates enums to include a Parlant type.

Changes

Cohort / File(s) Summary
Ignore and data placeholders
\.gitignore, parlant-data/cache_embeddings.json, parlant-data/evaluation_cache.json
Ignore parlant-data; add two empty JSON cache files.
Framework integration
runagent/sdk/server/framework/__init__.py, runagent/sdk/server/framework/parlant.py
Register and implement ParlantExecutor (subclass of GenericExecutor) and map "parlant" in executor registry.
Enums update
runagent/utils/enums.py
Add PARLANT = "parlant" to PythonicType.
Parlant template (default)
templates/parlant/default/README.md, templates/parlant/default/agent.py, templates/parlant/default/requirements.txt, templates/parlant/default/runagent.config.json
Add Parlant-based agent template with tools, chat/stream entrypoints, config, dependency, and README.
Template test harness
templates/parlant/test/test_chat.py
Add async E2E test to create agent, attach tools/guidelines, and exercise chat via local Parlant server.
Python client test
test_scripts/python/client_test_parlant.py
Add non-streaming and streaming tests using RunAgentClient with Parlant entrypoints.
Rust client test
test_scripts/rust/test_parlant/Cargo.toml, test_scripts/rust/test_parlant/src/main.rs
Add Rust project to test Parlant via RunAgentClient with run and run_stream flows.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant RunAgentClient as RunAgent Client
  participant RunAgent as RunAgent Server
  participant Exec as ParlantExecutor
  participant Parlant as Parlant Server

  User->>RunAgentClient: send(message, entrypoint_tag)
  RunAgentClient->>RunAgent: HTTP request (local)
  RunAgent->>Exec: dispatch(entrypoint_tag="parlant_simple"/"parlant_stream")
  Exec->>Parlant: create session / send message
  Parlant-->>Exec: events / response
  Exec-->>RunAgent: packaged result / stream chunks
  RunAgent-->>RunAgentClient: response / stream
  RunAgentClient-->>User: deliver content
Loading
sequenceDiagram
  autonumber
  participant Caller as Caller
  participant Agent as templates/parlant/default/agent.py
  participant PClient as AsyncParlantClient
  participant PServer as Parlant Server

  Caller->>Agent: simple_chat(message)
  activate Agent
  Agent->>Agent: get_or_create_agent()
  alt first run
    Agent->>Agent: get_parlant_client()
    Agent->>PServer: health/connectivity
    Agent->>Agent: create_agent_with_tools()
    Agent->>PServer: create agent + tools + guidelines
  end
  Agent->>Agent: chat_with_agent(message, agent_id)
  Agent->>PServer: create session, send user msg
  PServer-->>Agent: event stream
  Agent-->>Caller: {content, metadata}
  deactivate Agent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sawradip

Poem

A hop, a skip, to Parlant’s glade,
I wired the paths our agents made.
With tools in paws and streams that flow,
I test and chirp, “We’re good to go!”
Cache burrows ready, configs neat—
Thump-thump hooray for this new feat! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rad/parlant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (16)
templates/parlant/default/requirements.txt (1)

1-1: Pin the parlant dependency to a known-good version.

Prevents surprise breakages and supply‑chain drift.

Update once you confirm the tested version:

-parlant
+parlant==<tested_version>
.gitignore (1)

161-162: Scope the ignore rule and keep placeholders tracked.

Root-anchors avoid unintentionally ignoring similarly named paths; un-ignores keep the placeholder JSONs (or a .gitkeep) versioned.

Apply:

-#parlant
-parlant-data
+# Parlant caches
+/parlant-data/*
+!/parlant-data/cache_embeddings.json
+!/parlant-data/evaluation_cache.json
+!/parlant-data/.gitkeep
test_scripts/python/client_test_parlant.py (1)

3-4: Avoid hard-coded agent_id; make it configurable via env, reuse for both clients.

Improves portability of the script across environments and CI.

-from runagent import RunAgentClient
+from runagent import RunAgentClient
+import os
@@
-ra_simple = RunAgentClient(
-    agent_id="9c39620c-c309-4478-a44e-2a45e254a9fb",  # Replace with actual agent ID
+AGENT_ID = os.getenv("RA_AGENT_ID") or os.getenv("RUNAGENT_AGENT_ID") or "replace-with-your-agent-id"
+BASE_URL = os.getenv("RA_BASE_URL")  # optional
+
+ra_simple = RunAgentClient(
+    agent_id=AGENT_ID,
     entrypoint_tag="parlant_simple",
-    local=True
+    local=True if BASE_URL is None else False,
+    base_url=BASE_URL if BASE_URL else None,
 )
@@
-ra_stream = RunAgentClient(
-    agent_id="9c39620c-c309-4478-a44e-2a45e254a9fb",  # Replace with actual agent ID
+ra_stream = RunAgentClient(
+    agent_id=AGENT_ID,
     entrypoint_tag="parlant_stream",
-    local=True
+    local=True if BASE_URL is None else False,
+    base_url=BASE_URL if BASE_URL else None,
 )

Also applies to: 16-20, 42-46

templates/parlant/default/runagent.config.json (1)

27-30: Consider documenting required env vars in config or enforcing at startup.

If OPENAI_API_KEY is required, fail-fast with a clear message at agent init or document the requirement prominently.

templates/parlant/default/agent.py (4)

34-35: Replace print with logging.

Template code should not print or emit emojis. Use logging at INFO for creation/reuse messages.

-import print
+import logging
@@
-            print(f"✅ Found existing agent: {agent.name}")
+            logging.info("Found existing agent: %s", agent.name)
@@
-    print(f"✅ Created new agent: {agent.name}")
+    logging.info("Created new agent: %s", agent.name)
@@
-    print(f"✅ Added {len(guidelines)} guidelines and 3 tools")
+    logging.info("Added %d guidelines and 3 tools", len(guidelines))

Also applies to: 43-44, 156-156


239-246: Consistent error payload + explicit conversion flag.

Include entrypoint and avoid leaking internals; use {e!s}.

-    except Exception as e:
+    except Exception as e:
         return {
-            "content": f"Error processing message: {str(e)}",
+            "content": f"Error processing message: {e!s}",
             "type": "error",
             "error": str(e),
             "message_received": message,
-            "framework": "parlant"
+            "framework": "parlant",
+            "entrypoint": "simple_chat"
         }

271-278: Minor: unused loop index and streaming chunk shape.

  • Rename i to _i (Ruff B007).
  • Emit a consistent schema for deltas (type/chunk_type/agent_id) to help clients.
-        for i, word in enumerate(words):
-            yield {
-                "content": word + " "
-            }
+        for _i, word in enumerate(words):
+            yield {
+                "content": word + " ",
+                "type": "parlant_stream",
+                "chunk_type": "delta",
+                "agent_id": response.get("agent_id"),
+            }

290-297: Explicit conversion flag + consistent error shape.

Apply {e!s} and include entrypoint for parity.

-        yield {
-            "content": f"Error in streaming chat: {str(e)}",
+        yield {
+            "content": f"Error in streaming chat: {e!s}",
             "type": "error",
             "error": str(e),
             "message_received": message,
-            "framework": "parlant"
+            "framework": "parlant",
+            "entrypoint": "chat_stream"
         }
test_scripts/rust/test_parlant/Cargo.toml (1)

11-14: Dependency versions: prefer compatible majors and add newline at EOF.

  • Use caret/major versions so you get compatible bugfixes (e.g., tokio = "1", futures = "0.3").
  • Remove trailing spaces and ensure a newline at EOF.
-tokio = { version = "1.0", features = ["full"] }
+tokio = { version = "1", features = ["full"] }
 serde_json = "1.0"
 anyhow = "1.0"
-futures = "0.3"  
+futures = "0.3"
runagent/sdk/server/framework/__init__.py (2)

32-34: Registry entry LGTM; add trailing comma for cleaner diffs.

Functional as-is; a trailing comma avoids churn on future edits.

-        "n8n": N8NExecutor,
-        "parlant": ParlantExecutor
+        "n8n": N8NExecutor,
+        "parlant": ParlantExecutor,

35-38: Optional: richer error for unsupported frameworks.

Listing known keys helps users.

-    if framework_executor is None:
-        raise ValueError(f"Framework {framework} not supported yet.")
+    if framework_executor is None:
+        supported = ", ".join(sorted(executor_dict.keys()))
+        raise ValueError(f"Framework '{framework}' not supported. Supported: {supported}.")
test_scripts/rust/test_parlant/src/main.rs (1)

10-12: Avoid committing real agent IDs; parameterize via env/CLI

Hardcoding an agent UUID will break for other devs and leaks IDs in history.

Apply:

-    // Replace with the actual agent ID from `runagent serve`
-    let agent_id = "9c39620c-c309-4478-a44e-2a45e254a9fb";
+    // Agent ID from `runagent serve` (export RUNAGENT_AGENT_ID=...)
+    let agent_id = std::env::var("RUNAGENT_AGENT_ID").unwrap_or_else(|_| {
+        eprintln!("Set RUNAGENT_AGENT_ID env var to the UUID from `runagent serve`.");
+        std::process::exit(2);
+    });

Also pass by reference where needed:

-    let simple_client = RunAgentClient::new(
-        agent_id, 
+    let simple_client = RunAgentClient::new(
+        &agent_id, 
         "parlant_simple", 
         true  // local = true
     ).await?;
templates/parlant/test/test_chat.py (4)

1-6: Remove unused imports

time, datetime, and math are unused in this module (the tool code has its own imports).

 import asyncio
-import time
 from parlant.client import AsyncParlantClient
-from datetime import datetime
-import math

166-166: Remove f-string prefix with no placeholders

Static string; drop the f.

-        print(f"\n🎉 Done! Your agent is running at http://localhost:8800")
+        print("\n🎉 Done! Your agent is running at http://localhost:8800")

132-134: Narrow overly broad exception handling or annotate intent

Catching Exception hides real failures and trips BLE001. If the SDK exposes specific errors, catch those; otherwise add an inline ignore with rationale.

For example:

-    except Exception as e:
+    # NOTE: Parlant SDK may raise transport/HTTP errors; catch specific ones if available.
+    # noqa: BLE001
+    except Exception as e:
         return f"❌ Error: {e}"

and

-    except Exception as e:
+    # noqa: BLE001 – user-friendly CLI harness
+    except Exception as e:
         print(f"❌ Error: {e}")
         print("Make sure Parlant server is running: parlant-server run")

Also applies to: 169-171


138-139: Make base URL configurable

Hardcoding localhost hampers remote runs/CI.

-    client = AsyncParlantClient(base_url="http://localhost:8800")
+    import os
+    base_url = os.getenv("PARLANT_BASE_URL", "http://localhost:8800")
+    client = AsyncParlantClient(base_url=base_url)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2d55f6 and c61ed43.

📒 Files selected for processing (14)
  • .gitignore (1 hunks)
  • parlant-data/cache_embeddings.json (1 hunks)
  • parlant-data/evaluation_cache.json (1 hunks)
  • runagent/sdk/server/framework/__init__.py (2 hunks)
  • runagent/sdk/server/framework/parlant.py (1 hunks)
  • runagent/utils/enums.py (1 hunks)
  • templates/parlant/default/README.md (1 hunks)
  • templates/parlant/default/agent.py (1 hunks)
  • templates/parlant/default/requirements.txt (1 hunks)
  • templates/parlant/default/runagent.config.json (1 hunks)
  • templates/parlant/test/test_chat.py (1 hunks)
  • test_scripts/python/client_test_parlant.py (1 hunks)
  • test_scripts/rust/test_parlant/Cargo.toml (1 hunks)
  • test_scripts/rust/test_parlant/src/main.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
runagent/sdk/server/framework/parlant.py (1)
runagent/sdk/server/framework/generic.py (1)
  • GenericExecutor (14-147)
runagent/sdk/server/framework/__init__.py (1)
runagent/sdk/server/framework/parlant.py (1)
  • ParlantExecutor (5-21)
test_scripts/rust/test_parlant/src/main.rs (1)
templates/parlant/test/test_chat.py (1)
  • main (136-171)
templates/parlant/default/agent.py (1)
templates/parlant/test/test_chat.py (2)
  • create_agent_with_tools (8-97)
  • chat_with_agent (100-133)
templates/parlant/test/test_chat.py (1)
templates/parlant/default/agent.py (2)
  • create_agent_with_tools (27-157)
  • chat_with_agent (174-209)
🪛 markdownlint-cli2 (0.17.2)
templates/parlant/default/README.md

26-26: Bare URL used

(MD034, no-bare-urls)


32-32: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


36-36: Bare URL used

(MD034, no-bare-urls)


54-54: Bare URL used

(MD034, no-bare-urls)


57-57: Bare URL used

(MD034, no-bare-urls)


58-58: Bare URL used

(MD034, no-bare-urls)

🪛 Ruff (0.12.2)
templates/parlant/default/agent.py

21-21: Do not catch blind exception: Exception

(BLE001)


22-22: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


22-22: Create your own exception

(TRY002)


22-22: Avoid specifying long messages outside the exception class

(TRY003)


208-208: Do not catch blind exception: Exception

(BLE001)


209-209: Use explicit conversion flag

Replace with conversion flag

(RUF010)


239-239: Do not catch blind exception: Exception

(BLE001)


241-241: Use explicit conversion flag

Replace with conversion flag

(RUF010)


271-271: Loop control variable i not used within loop body

Rename unused i to _i

(B007)


290-290: Do not catch blind exception: Exception

(BLE001)


292-292: Use explicit conversion flag

Replace with conversion flag

(RUF010)

templates/parlant/test/test_chat.py

132-132: Do not catch blind exception: Exception

(BLE001)


166-166: f-string without any placeholders

Remove extraneous f prefix

(F541)


169-169: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (7)
parlant-data/evaluation_cache.json (1)

1-1: LGTM: placeholder cache.

Empty JSON is fine for a tracked placeholder.

parlant-data/cache_embeddings.json (1)

1-1: LGTM: placeholder cache.

Empty JSON is acceptable for initial scaffolding.

runagent/utils/enums.py (1)

21-21: LGTM: PythonicType.PARLANT added.

Enum addition aligns with the new framework.

templates/parlant/default/README.md (1)

26-27: Tighten Markdown formatting, present notes as callouts, and use hyphenated CLI
Use parlant-server run and convert bare URLs to inline links or wrap them in angle brackets; present terminal notes as callouts. Applies also at lines 36 and 54–58.

-*Keep this terminal running. Server will start at http://localhost:8800*
+> Note: Keep this terminal running. The server starts at http://localhost:8800.
@@
-Go to the runagent templates https://github.com/runagent-dev/runagent/tree/main/templates to try
+Go to the [RunAgent templates](https://github.com/runagent-dev/runagent/tree/main/templates) to try
@@
-RunAgent GitHub Issues: https://github.com/runagent-dev/runagent/issues
+RunAgent GitHub Issues: <https://github.com/runagent-dev/runagent/issues>
@@
-Parlant Discord: https://discord.gg/parlant  
-Parlant GitHub Issues: https://github.com/emcie-co/parlant/issues
+Parlant Discord: <https://discord.gg/parlant>  
+Parlant GitHub Issues: <https://github.com/emcie-co/parlant/issues>
templates/parlant/default/agent.py (1)

119-148: Guidelines look solid.

Clear mapping between intents and tools; good coverage for common flows.

runagent/sdk/server/framework/__init__.py (1)

14-14: Importing ParlantExecutor is correct.

The import wiring matches the new executor module.

runagent/sdk/server/framework/parlant.py (1)

5-21: LGTM: minimal executor is appropriate

Subclassing GenericExecutor without overrides is correct for Parlant’s async/sync parity. No issues found.

Comment on lines +15 to +23
if _parlant_client is None:
_parlant_client = AsyncParlantClient(base_url="http://localhost:8800")

# Test connection
try:
await _parlant_client.agents.list()
except Exception as e:
raise Exception(f"Failed to connect to Parlant server at http://localhost:8800. Make sure it's running. Error: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve client init: guard re-entrancy, configurable base URL, and preserve traceback.

  • Add an async init lock to avoid double-initialization under concurrency.
  • Read base URL from env with a sensible default.
  • Use raise RuntimeError(... ) from e for context.
-    if _parlant_client is None:
-        _parlant_client = AsyncParlantClient(base_url="http://localhost:8800")
+    if _parlant_client is None:
+        import os
+        base_url = os.getenv("PARLANT_BASE_URL", "http://localhost:8800")
+        _parlant_client = AsyncParlantClient(base_url=base_url)
@@
-        except Exception as e:
-            raise Exception(f"Failed to connect to Parlant server at http://localhost:8800. Make sure it's running. Error: {e}")
+        except Exception as e:
+            raise RuntimeError(f"Failed to connect to Parlant server. Make sure it's running at {base_url}.") from e

Optionally, declare a module-level lock and use it here:

-# Global client and agent_id for reuse
-_parlant_client = None
+_parlant_client = None
+_client_init_lock = asyncio.Lock()
@@
-    if _parlant_client is None:
+    async with _client_init_lock:
+        if _parlant_client is not None:
+            return _parlant_client
+        # (init code here)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if _parlant_client is None:
_parlant_client = AsyncParlantClient(base_url="http://localhost:8800")
# Test connection
try:
await _parlant_client.agents.list()
except Exception as e:
raise Exception(f"Failed to connect to Parlant server at http://localhost:8800. Make sure it's running. Error: {e}")
if _parlant_client is None:
import os
base_url = os.getenv("PARLANT_BASE_URL", "http://localhost:8800")
_parlant_client = AsyncParlantClient(base_url=base_url)
# Test connection
try:
await _parlant_client.agents.list()
except Exception as e:
raise RuntimeError(
f"Failed to connect to Parlant server. Make sure it's running at {base_url}."
) from e
🧰 Tools
🪛 Ruff (0.12.2)

21-21: Do not catch blind exception: Exception

(BLE001)


22-22: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


22-22: Create your own exception

(TRY002)


22-22: Avoid specifying long messages outside the exception class

(TRY003)

Comment on lines +60 to +71
implementation="""
try:
import math
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})

expression = expression.replace("^", "**")
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {str(e)}"
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Block unsafe eval in calculator tool; use an AST-based evaluator.

Using eval, even with restricted builtins, is not safe against attribute/magic access. Replace the tool implementation with a whitelist AST evaluator.

-        implementation="""
-try:
-    import math
-    allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
-    allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})
-    
-    expression = expression.replace("^", "**")
-    result = eval(expression, {"__builtins__": {}}, allowed_names)
-    return f"Result: {result}"
-except Exception as e:
-    return f"Error calculating '{expression}': {str(e)}"
-"""
+        implementation="""
+import ast, math, operator
+
+ALLOWED_FUNCS = {
+    "abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow,
+    **{k: v for k, v in math.__dict__.items() if not k.startswith("_")}
+}
+ALLOWED_BIN = {
+    ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
+    ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
+    ast.Mod: operator.mod, ast.Pow: operator.pow,
+}
+ALLOWED_UN = {ast.UAdd: operator.pos, ast.USub: operator.neg}
+
+def _eval(node):
+    if isinstance(node, ast.Num):  # py<3.8
+        return node.n
+    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
+        return node.value
+    if isinstance(node, ast.BinOp) and type(node.op) in ALLOWED_BIN:
+        return ALLOWED_BIN[type(node.op)](_eval(node.left), _eval(node.right))
+    if isinstance(node, ast.UnaryOp) and type(node.op) in ALLOWED_UN:
+        return ALLOWED_UN[type(node.op)](_eval(node.operand))
+    if isinstance(node, ast.Name) and node.id in ALLOWED_FUNCS:
+        return ALLOWED_FUNCS[node.id]
+    if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
+        func = _eval(node.func)
+        args = [_eval(a) for a in node.args]
+        if node.keywords:
+            raise ValueError("Keywords not allowed")
+        return func(*args)
+    raise ValueError("Disallowed expression")
+
+try:
+    expr = expression.replace("^", "**")
+    tree = ast.parse(expr, mode="eval")
+    result = _eval(tree.body)
+    return f"Result: {result}"
+except Exception as e:
+    return f"Error calculating '{expression}': {e!s}"
+"""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation="""
try:
import math
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})
expression = expression.replace("^", "**")
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {str(e)}"
"""
implementation="""
import ast, math, operator
ALLOWED_FUNCS = {
"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow,
**{k: v for k, v in math.__dict__.items() if not k.startswith("_")}
}
ALLOWED_BIN = {
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod, ast.Pow: operator.pow,
}
ALLOWED_UN = {ast.UAdd: operator.pos, ast.USub: operator.neg}
def _eval(node):
if isinstance(node, ast.Num): # py<3.8
return node.n
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in ALLOWED_BIN:
return ALLOWED_BIN[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in ALLOWED_UN:
return ALLOWED_UN[type(node.op)](_eval(node.operand))
if isinstance(node, ast.Name) and node.id in ALLOWED_FUNCS:
return ALLOWED_FUNCS[node.id]
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
func = _eval(node.func)
args = [_eval(a) for a in node.args]
if node.keywords:
raise ValueError("Keywords not allowed")
return func(*args)
raise ValueError("Disallowed expression")
try:
expr = expression.replace("^", "**")
tree = ast.parse(expr, mode="eval")
result = _eval(tree.body)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {e!s}"
"""
🤖 Prompt for AI Agents
In templates/parlant/default/agent.py around lines 60 to 71, the current
calculator uses eval with a filtered math dict which is unsafe; replace it with
an AST-based evaluator: parse the expression with ast.parse in 'eval' mode, walk
the AST and only allow a strict whitelist of node types (e.g., Expression,
BinOp, UnaryOp, Constant/Num, Name, Call) and operators (Add, Sub, Mult, Div,
Pow, Mod, FloorDiv, UAdd, USub), reject any Attribute, Subscript, Lambda,
comprehension, or other nodes; resolve Name nodes only from an explicit safe
mapping of math functions/constants (no __* names) and allow Call nodes only if
the function is in that mapping and arguments are validated recursively;
evaluate the AST recursively to compute the result and raise a clear error on
any disallowed node or name instead of using eval.

Comment on lines +178 to +210
try:
# Create a chat session
session = await client.sessions.create(
agent_id=agent_id,
allow_greeting=False
)

# Send message
event = await client.sessions.create_event(
session_id=session.id,
kind="message",
source="customer",
message=message,
)

# Get response
agent_messages = await client.sessions.list_events(
session_id=session.id,
min_offset=event.offset,
source="ai_agent",
kinds="message",
wait_for_data=30,
)

if agent_messages:
response_data = agent_messages[0].model_dump()
return response_data.get("data", {}).get("message", "No response received")
else:
return "No response from agent"

except Exception as e:
return f"Error processing message: {str(e)}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid broad except; return structured error and keep traceback in logs.

Use narrower exceptions if the SDK defines them; otherwise log the exception and return a stable error payload. Also apply explicit conversion flags in f-strings (RUF010).

-    except Exception as e:
-        return f"Error processing message: {str(e)}"
+    except Exception as e:
+        import logging
+        logging.exception("chat_with_agent failed")
+        return f"Error processing message: {e!s}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
# Create a chat session
session = await client.sessions.create(
agent_id=agent_id,
allow_greeting=False
)
# Send message
event = await client.sessions.create_event(
session_id=session.id,
kind="message",
source="customer",
message=message,
)
# Get response
agent_messages = await client.sessions.list_events(
session_id=session.id,
min_offset=event.offset,
source="ai_agent",
kinds="message",
wait_for_data=30,
)
if agent_messages:
response_data = agent_messages[0].model_dump()
return response_data.get("data", {}).get("message", "No response received")
else:
return "No response from agent"
except Exception as e:
return f"Error processing message: {str(e)}"
try:
# Create a chat session
session = await client.sessions.create(
agent_id=agent_id,
allow_greeting=False
)
# Send message
event = await client.sessions.create_event(
session_id=session.id,
kind="message",
source="customer",
message=message,
)
# Get response
agent_messages = await client.sessions.list_events(
session_id=session.id,
min_offset=event.offset,
source="ai_agent",
kinds="message",
wait_for_data=30,
)
if agent_messages:
response_data = agent_messages[0].model_dump()
return response_data.get("data", {}).get("message", "No response received")
else:
return "No response from agent"
except Exception as e:
import logging
logging.exception("chat_with_agent failed")
return f"Error processing message: {e!s}"
🧰 Tools
🪛 Ruff (0.12.2)

208-208: Do not catch blind exception: Exception

(BLE001)


209-209: Use explicit conversion flag

Replace with conversion flag

(RUF010)

Comment on lines +16 to +24
"file": "agent.py",
"module": "simple_chat",
"tag": "parlant_simple"
},
{
"file": "agent.py",
"module": "chat_stream",
"tag": "parlant_stream"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify entrypoint symbols exist and match exports.

Ensure simple_chat and chat_stream are defined in templates/parlant/default/agent.py and importable.


🏁 Script executed:

#!/bin/bash
# Verify entrypoints exist and are functions
rg -nP '^\s*def\s+simple_chat\s*\(' templates/parlant/default/agent.py -C2
rg -nP '^\s*def\s+chat_stream\s*\(' templates/parlant/default/agent.py -C2

# Verify framework wiring exists
rg -nP 'ParlantExecutor' runagent/sdk/server/framework -C2
rg -nP '"parlant"\s*:\s*ParlantExecutor' runagent/sdk/server/framework/__init__\.py -n

# Sanity-check enum usage
rg -nP 'PythonicType\.PARLANT' -n

Length of output: 1217


🏁 Script executed:

#!/bin/bash
# List files in the templates/parlant/default directory
ls -l templates/parlant/default

# Display the first 200 lines of agent.py
sed -n '1,200p' templates/parlant/default/agent.py

# Show all function definitions in agent.py
rg -nP '^\s*def ' templates/parlant/default/agent.py -n

Length of output: 6768


Define missing entrypoints or update config
The runagent.config.json references simple_chat and chat_stream in templates/parlant/default/agent.py, but neither function is defined. Add implementations for these entrypoints in agent.py or remove/rename them in the config.

🤖 Prompt for AI Agents
In templates/parlant/default/runagent.config.json around lines 16 to 24, the
config references entrypoints "simple_chat" and "chat_stream" that are not
implemented in templates/parlant/default/agent.py; either add corresponding
exported functions named simple_chat(...) and chat_stream(...) in agent.py with
the expected signatures (matching how the framework loads entrypoints) or update
the JSON to reference existing functions/modules or remove those
entries—implementations should accept the agent runtime/context parameters and
return the appropriate handler objects/callables, and if you choose to
rename/remove, ensure the config and any loader code remain consistent.

Comment on lines +41 to +52
implementation="""
try:
import math
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})

expression = expression.replace("^", "**")
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {str(e)}"
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security: avoid eval on untrusted expressions in tool implementation

Even with restricted builtins, eval increases risk. Use an AST-based evaluator with an allowlist of nodes and math functions.

Replace the implementation block:

-        implementation="""
-try:
-    import math
-    allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
-    allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})
-    
-    expression = expression.replace("^", "**")
-    result = eval(expression, {"__builtins__": {}}, allowed_names)
-    return f"Result: {result}"
-except Exception as e:
-    return f"Error calculating '{expression}': {str(e)}"
-"""
+        implementation="""
+import ast, operator as op, math
+
+OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg}
+ALLOWED = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
+ALLOWED.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum})
+
+def _eval(node):
+    if isinstance(node, ast.Num):  # py<3.8
+        return node.n
+    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
+        return node.value
+    if isinstance(node, ast.BinOp) and type(node.op) in OPS:
+        return OPS[type(node.op)](_eval(node.left), _eval(node.right))
+    if isinstance(node, ast.UnaryOp) and type(node.op) in OPS:
+        return OPS[type(node.op)](_eval(node.operand))
+    if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ALLOWED:
+        return ALLOWED[node.func.id](*[_eval(a) for a in node.args])
+    raise ValueError("Unsupported expression")
+
+try:
+    expr = expression.replace("^", "**")
+    result = _eval(ast.parse(expr, mode="eval").body)
+    return f"Result: {result}"
+except Exception as e:
+    return f"Error calculating '{expression}': {str(e)}"
+"""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation="""
try:
import math
allowed_names = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
allowed_names.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum, "pow": pow})
expression = expression.replace("^", "**")
result = eval(expression, {"__builtins__": {}}, allowed_names)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {str(e)}"
"""
implementation="""
import ast, operator as op, math
OPS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.Pow: op.pow,
ast.USub: op.neg
}
ALLOWED = {k: v for k, v in math.__dict__.items() if not k.startswith("__")}
ALLOWED.update({"abs": abs, "round": round, "min": min, "max": max, "sum": sum})
def _eval(node):
if isinstance(node, ast.Num): # py<3.8
return node.n
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in OPS:
return OPS[type(node.op)](_eval(node.operand))
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ALLOWED:
return ALLOWED[node.func.id](*[_eval(a) for a in node.args])
raise ValueError("Unsupported expression")
try:
expr = expression.replace("^", "**")
result = _eval(ast.parse(expr, mode="eval").body)
return f"Result: {result}"
except Exception as e:
return f"Error calculating '{expression}': {str(e)}"
"""
🤖 Prompt for AI Agents
In templates/parlant/test/test_chat.py around lines 41 to 52, the current
implementation uses eval on untrusted expressions; replace it with an AST-based
evaluator: parse the expression with ast.parse(mode='eval'), walk the AST to
only allow a narrow whitelist of node types (e.g., Expression, BinOp, UnaryOp,
Call, Name, Load, Constant, operators and comparators you need) and reject any
other nodes, allow only a predefined mapping of safe math functions/constants
(from math plus abs/round/min/max/sum/pow) for Name/Call resolution, then
evaluate the validated AST either by compiling the sanitized AST to a code
object or by implementing a recursive evaluator that computes results from AST
nodes and returns the result string; ensure all parsing and validation errors
produce the same error-return format used today.


# Test messages
test_messages = [
"Hey babe how can you help me?",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inappropriate user-facing copy

“Hey babe…” is unprofessional for a sample. Use a neutral greeting.

-            "Hey babe how can you help me?",
+            "Hey, how can you help me?",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Hey babe how can you help me?",
"Hey, how can you help me?",
🤖 Prompt for AI Agents
In templates/parlant/test/test_chat.py around line 151, the sample user message
uses an unprofessional greeting ("Hey babe how can you help me?"); replace it
with a neutral greeting such as "Hello, how can you help me?" or "Hi, how can
you help me?" so the test uses professional, non-personal language and keep
sentence punctuation consistent.


[dependencies]
# Assuming you're using the runagent Rust SDK
runagent = { path = "/home/riamdriad5/runagent/runagent/runagent-rust/runagent" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Absolute path to runagent crate breaks portability/CI.

Use a relative path, a workspace member, or a git dependency. Absolute home paths will fail for other devs and CI.

-runagent = { path = "/home/riamdriad5/runagent/runagent/runagent-rust/runagent" }
+# Prefer a relative path (adjust after verifying actual location), or switch to a git dependency
+runagent = { path = "../../../runagent-rust/runagent" }
+# Alternative:
+# runagent = { git = "https://github.com/runagent-dev/runagent-rust.git", rev = "<pin>" }

To locate the correct relative path in this repo, run:


🏁 Script executed:

#!/bin/bash
# Find the Rust crate named runagent to compute a proper relative path.
fd -H --type d '^runagent$' | rg -n '' -C0

Length of output: 61


Use a relative path for the runagent crate

Remove the absolute path and update to the correct relative path from test_scripts/rust/test_parlant/Cargo.toml:

- runagent = { path = "/home/riamdriad5/runagent/runagent/runagent-rust/runagent" }
+ runagent = { path = "../../../runagent-rust/runagent" }
# Alternative git dependency:
# runagent = { git = "https://github.com/runagent-dev/runagent-rust.git", rev = "<pin>" }
🤖 Prompt for AI Agents
In test_scripts/rust/test_parlant/Cargo.toml around line 8, the runagent
dependency uses an absolute filesystem path; replace it with the correct
relative path from test_scripts/rust/test_parlant to the runagent crate (e.g.,
../../.. or the appropriate relative traversal into the
runagent/runagent-rust/runagent directory) by updating runagent = { path =
"<relative/path/to/runagent>" } so the crate is referenced portably and builds
on other machines.

Comment on lines +45 to +81
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Testing Parlant Agent with Rust SDK");
println!("=======================================");

// Replace with the actual agent ID from `runagent serve`
let agent_id = "9c39620c-c309-4478-a44e-2a45e254a9fb";

// Test 5: Streaming Chat
println!("\n5️⃣ Testing Streaming Chat");
println!("{}", "-".repeat(30));

let stream_client = RunAgentClient::new(
agent_id,
"parlant_stream",
true
).await?;

let mut stream = stream_client.run_stream(&[
("message", json!("can you tell me the sum of 10 to 20."))
]).await?;

println!("✅ Streaming Response:");
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => {
print!("{}", chunk);
},
Err(e) => {
println!("❌ Stream Error: {}", e);
break;
}
}
}

Ok(())
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix: duplicate #[tokio::main] and duplicate main cause compile/link errors

Rust crates can only have one main. The second block redefines main and repeats imports.

Apply these changes to fold streaming into the first main and remove duplicates:

-// ############################streaming##################################
-
-use runagent::client::RunAgentClient;
-use serde_json::json;
-use futures::StreamExt;
-
-#[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
-    println!("🧪 Testing Parlant Agent with Rust SDK");
-    println!("=======================================");
-    
-    // Replace with the actual agent ID from `runagent serve`
-    let agent_id = "9c39620c-c309-4478-a44e-2a45e254a9fb";
-    
-    // Test 5: Streaming Chat
-    println!("\n5️⃣ Testing Streaming Chat");
-    println!("{}", "-".repeat(30));
-    
-    let stream_client = RunAgentClient::new(
-        agent_id,
-        "parlant_stream",
-        true
-    ).await?;
-
-    let mut stream = stream_client.run_stream(&[
-        ("message", json!("can you tell me the sum of 10 to 20."))
-    ]).await?;
-
-    println!("✅ Streaming Response:");
-    while let Some(chunk_result) = stream.next().await {
-        match chunk_result {
-            Ok(chunk) => {
-                print!("{}", chunk);
-            },
-            Err(e) => {
-                println!("❌ Stream Error: {}", e);
-                break;
-            }
-        }
-    }
-    
-    Ok(())
-}

And extend the first main before Ok(()):

     println!("{}", serde_json::to_string_pretty(&simple_response)?);
     
-    
-    Ok(())
+    // ---- Streaming Chat ----
+    println!("\n5️⃣ Testing Streaming Chat");
+    println!("{}", "-".repeat(30));
+
+    let stream_client = RunAgentClient::new(
+        &agent_id,
+        "parlant_stream",
+        true
+    ).await?;
+
+    let mut stream = stream_client.run_stream(&[
+        ("message", json!("can you tell me the sum of 10 to 20.")),
+    ]).await?;
+
+    println!("✅ Streaming Response:");
+    while let Some(chunk_result) = stream.next().await {
+        match chunk_result {
+            Ok(chunk) => print!("{}", chunk),
+            Err(e) => {
+                eprintln!("❌ Stream Error: {}", e);
+                break;
+            }
+        }
+    }
+
+    Ok(())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🧪 Testing Parlant Agent with Rust SDK");
println!("=======================================");
// Replace with the actual agent ID from `runagent serve`
let agent_id = "9c39620c-c309-4478-a44e-2a45e254a9fb";
// Test 5: Streaming Chat
println!("\n5️⃣ Testing Streaming Chat");
println!("{}", "-".repeat(30));
let stream_client = RunAgentClient::new(
agent_id,
"parlant_stream",
true
).await?;
let mut stream = stream_client.run_stream(&[
("message", json!("can you tell me the sum of 10 to 20."))
]).await?;
println!("✅ Streaming Response:");
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => {
print!("{}", chunk);
},
Err(e) => {
println!("❌ Stream Error: {}", e);
break;
}
}
}
Ok(())
}
println!("{}", serde_json::to_string_pretty(&simple_response)?);
// ---- Streaming Chat ----
println!("\n5️⃣ Testing Streaming Chat");
println!("{}", "-".repeat(30));
let stream_client = RunAgentClient::new(
&agent_id,
"parlant_stream",
true
).await?;
let mut stream = stream_client.run_stream(&[
("message", json!("can you tell me the sum of 10 to 20.")),
]).await?;
println!("✅ Streaming Response:");
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => print!("{}", chunk),
Err(e) => {
eprintln!("❌ Stream Error: {}", e);
break;
}
}
}
Ok(())
🤖 Prompt for AI Agents
In test_scripts/rust/test_parlant/src/main.rs around lines 45 to 81, there is a
duplicate #[tokio::main] and a second main function causing compile/link errors;
remove the second #[tokio::main] and the duplicate main definition and any
repeated imports, then move the Streaming Chat block (creating RunAgentClient,
calling run_stream and iterating over stream.next()) into the existing first
async main before its Ok(()) so all tests run in one async main; ensure any
necessary use statements (e.g., futures::StreamExt) are present once at the top
and that awaits remain valid.

@sawradip
sawradip merged commit 61f8eea into main Sep 8, 2025
2 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Oct 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants