feat: Added support for Parlant Framework#66
Conversation
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. Comment |
There was a problem hiding this comment.
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/.gitkeeptest_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/CLIHardcoding 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, andmathare 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 placeholdersStatic 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 intentCatching
Exceptionhides 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 configurableHardcoding 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
📒 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
Useparlant-server runand 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 appropriateSubclassing GenericExecutor without overrides is correct for Parlant’s async/sync parity. No issues found.
| 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}") | ||
|
|
There was a problem hiding this comment.
🛠️ 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 eOptionally, 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.
| 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)
| 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)}" | ||
| """ |
There was a problem hiding this comment.
🛠️ Refactor suggestion
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.
| 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.
| 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)}" | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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)
| "file": "agent.py", | ||
| "module": "simple_chat", | ||
| "tag": "parlant_simple" | ||
| }, | ||
| { | ||
| "file": "agent.py", | ||
| "module": "chat_stream", | ||
| "tag": "parlant_stream" | ||
| } |
There was a problem hiding this comment.
💡 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' -nLength 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 -nLength 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.
| 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)}" | ||
| """ |
There was a problem hiding this comment.
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.
| 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?", |
There was a problem hiding this comment.
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.
| "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" } |
There was a problem hiding this comment.
💡 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 '' -C0Length 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.
| #[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 |
There was a problem hiding this comment.
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.
| #[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.
Added support of parlant framework tested in python, rust and go and capable of both streaming and non-streaming support
Summary by CodeRabbit