Add Microsoft Teams integration. Fixes #88#111
Add Microsoft Teams integration. Fixes #88#111striver-24 wants to merge 1 commit intojentic:mainfrom
Conversation
- Add teams_agent.py with Bot Framework integration - Include enterprise features: rate limiting, input validation, error handling - Add requirements.txt with production-ready dependencies - Add comprehensive README.md with setup documentation - Follow established patterns from Slack/Discord examples
|
Thanks so much for this PR and for taking the time to contribute to Standard Agent — welcome aboard 🙌 |
|
Thank you for the warm welcome! I’m excited to contribute more to Standard Agent and look forward to your feedback. 😊 |
|
@CodeRabbit review this PR |
|
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughA new Microsoft Teams bot example is added following patterns from existing Slack/Discord examples. The implementation includes configuration, agent runtime with profile selection, rate limiting, command handling, authentication, and a complete HTTP server for Teams integration. Changes
Sequence DiagramsequenceDiagram
actor Teams as Teams User
participant Bot as TeamsAgentBot
participant Limiter as RateLimiter
participant Agent as StandardAgent
participant Teams_API as Teams Channel
Teams->>Bot: on_message_activity()
activate Bot
Bot->>Limiter: is_allowed(user_id)
activate Limiter
Limiter-->>Bot: allowed: bool
deactivate Limiter
alt rate limit exceeded
Bot-->>Teams_API: send error message
else allowed
Bot->>Bot: extract_goal_from_mention()
Bot->>Bot: _build_agent(profile)
activate Agent
Bot->>Agent: process goal with timeout
Agent-->>Bot: response text
deactivate Agent
Bot->>Bot: chunk_text(response)
Bot->>Teams_API: send chunked responses
end
deactivate Bot
Teams_API-->>Teams: display message(s)
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~30 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@examples/teams/requirements.txt`:
- Around line 1-18: The cryptography dependency line currently reads
"cryptography>=41.0.0" and lacks an upper bound; update that requirement to
include an upper bound (e.g., change to "cryptography>=41.0.0,<47.0.0") so the
cryptography package is constrained to a safe major-version range.
In `@examples/teams/teams_agent.py`:
- Around line 267-271: The except block that calls
logger.exception("teams_message_error", error=str(exc), user_id=user_id) can
raise a NameError if user_id wasn't set earlier; update the except handler in
the teams_agent.py except Exception as exc block to resolve user_id safely
(e.g., derive it from turn_context.activity.from_property.id if present, or use
a fallback like None or "unknown") before passing it to logger.exception and
sending the error message; ensure you reference the same safe variable name
(user_id) when logging so the logger call can never reference an undefined
symbol.
- Around line 513-518: The exception handler references turn_context which may
not be defined if the error occurs before it's created; update the except block
in the same scope to safely handle that by checking for the variable (e.g., if
"turn_context" in locals() and turn_context is not None) before calling
turn_context.send_activity, otherwise fall back to a safe alternative such as
using the bot adapter to send a proactive message or simply logging the error
and returning web.Response(status=200); ensure the check uses the exact symbol
turn_context and that any fallback uses existing adapter/logger symbols
available in the surrounding function.
- Around line 404-440: The authenticate_request function currently does only a
superficial token-length check and ignores app_credentials; replace that stub
with a real Bot Framework JWT validation by parsing the incoming request body
into an Activity (e.g., await request.json() -> Activity(...)) and calling await
JwtTokenValidation.authenticate_request(activity, auth_header, app_credentials);
return True when that call succeeds, catch and log JwtTokenValidation exceptions
and return False on failure, ensure the app_credentials argument is used, and if
you keep the TEAMS_DEV_MODE bypass preserve it only for explicit local dev use
with a clear log entry.
🧹 Nitpick comments (5)
examples/teams/teams_agent.py (3)
75-84: Unusedrate_limiterfield inTeamsAgentRuntime.The
rate_limiterfield is initialized in__post_init__but never used. The actual rate limiting is performed by a separateRateLimiterinstance created inTeamsAgentBot.__init__(line 167). This is confusing and appears to be dead code.Consider removing the unused field:
♻️ Proposed fix
`@dataclass`(slots=True) class TeamsAgentRuntime: chosen_profile: ReasonerProfile = ReasonerProfile.REWOO current_agent: Optional[StandardAgent] = None app_credentials: Optional[MicrosoftAppCredentials] = None - rate_limiter: Dict[str, deque] = None - - def __post_init__(self): - if self.rate_limiter is None: - self.rate_limiter = defaultdict(deque)
87-117: Consider cleanup mechanism for inactive users inRateLimiter.The
requestsdictionary grows unboundedly as new users interact with the bot. While individual user deques are pruned of old timestamps, entries for inactive users are never removed. In a long-running bot with many transient users, this could lead to gradual memory growth.For a production deployment, consider adding periodic cleanup of users with empty deques or using a TTL-based cache.
143-156: Unusedbot_mentionparameter.The
bot_mentionparameter is declared but never used in the function body. Either remove it or implement the intended functionality (e.g., removing a specific bot mention pattern).♻️ Proposed fix
-def extract_goal_from_mention(text: str, bot_mention: str = None) -> str: +def extract_goal_from_mention(text: str) -> str: """Extract goal from message text, removing bot mentions.""" if not text: return ""examples/teams/README.md (2)
204-226: Add language specifiers to fenced code blocks.The usage example code blocks (lines 208-210, 214-216, 220-226) are missing language specifiers, which affects syntax highlighting and linting.
📝 Proposed fix
### In Teams Channels or Group Chats Mention the bot with your goal: -``` +```text `@StandardAgent` find recent articles about machine learningDirect Messages
Send a direct message to the bot:
-+text
Help me plan a software architecture for a web application### Commands Use slash commands for configuration: -``` +```text /help /configure /reasoner react /reasoner list /kill</details> --- `341-346`: **Rate limiting is already implemented — update documentation.** The documentation suggests "consider implementing rate limiting" as a future consideration, but the code already implements per-user rate limiting in `RateLimiter` class (20 requests per 300 seconds). Update this section to reflect the actual implementation. <details> <summary>📝 Proposed fix</summary> ```diff ### Rate Limiting -Consider implementing rate limiting to prevent abuse: -- Per-user rate limits -- Per-conversation rate limits -- Global rate limits +Rate limiting is implemented to prevent abuse: +- Per-user rate limits: 20 requests per 5 minutes (configurable) + +Consider adding for enhanced protection: +- Per-conversation rate limits +- Global rate limits
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
examples/teams/README.mdexamples/teams/requirements.txtexamples/teams/teams_agent.py
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
examples/teams/README.md
208-208: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
214-214: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
220-220: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🪛 Ruff (0.14.11)
examples/teams/teams_agent.py
71-71: Avoid specifying long messages outside the exception class
(TRY003)
143-143: Unused function argument: bot_mention
(ARG001)
143-143: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
225-225: Do not catch blind exception: Exception
(BLE001)
226-226: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
251-251: Do not catch blind exception: Exception
(BLE001)
252-252: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
276-276: Do not catch blind exception: Exception
(BLE001)
277-277: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
395-395: Do not catch blind exception: Exception
(BLE001)
396-396: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
404-404: Unused function argument: app_credentials
(ARG001)
432-432: Consider moving this statement to an else block
(TRY300)
434-434: Do not catch blind exception: Exception
(BLE001)
435-435: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
438-438: Do not catch blind exception: Exception
(BLE001)
439-439: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
484-484: Do not catch blind exception: Exception
(BLE001)
485-485: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
494-494: Do not catch blind exception: Exception
(BLE001)
495-495: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
524-524: Unused function argument: request
(ARG001)
549-549: Possible binding to all interfaces
(S104)
573-573: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (3)
examples/teams/teams_agent.py (2)
549-549: Binding to all interfaces is intentional for container deployment.The
0.0.0.0binding is appropriate for Docker/Kubernetes deployments where the container needs to accept connections from the host network. The README documents this deployment pattern.
1-51: Good overall structure and patterns.The implementation follows a clean architecture with proper separation of concerns:
- Configuration loading via
TeamsConfig.from_env()- Runtime state management via
TeamsAgentRuntime- Bot handler with comprehensive error handling
- Input validation and rate limiting
- Async patterns with proper timeout handling
The code aligns well with the existing Slack/Discord examples as intended by the issue requirements.
examples/teams/README.md (1)
1-4: Comprehensive and well-structured documentation.The README provides thorough setup instructions covering Azure bot registration, Teams app manifest creation, local development with ngrok, and deployment options. This meets the acceptance criteria from issue
#88for setup documentation.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| # Microsoft Bot Framework dependencies | ||
| botbuilder-core>=4.15.0,<5.0.0 | ||
| botframework-connector>=4.15.0,<5.0.0 | ||
| botbuilder-schema>=4.15.0,<5.0.0 | ||
|
|
||
| # Web server dependencies | ||
| aiohttp>=3.8.0,<4.0.0 | ||
| aiohttp-cors>=0.7.0,<1.0.0 | ||
|
|
||
| # Configuration and environment | ||
| python-dotenv>=1.0.0,<2.0.0 | ||
|
|
||
| # Security and authentication (optional but recommended) | ||
| cryptography>=41.0.0 | ||
|
|
||
| # Development and testing (optional) | ||
| pytest>=7.0.0 | ||
| pytest-asyncio>=0.21.0 No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and look at its exact content including the end
cat -n examples/teams/requirements.txt | tail -5
# Also check with hexdump to see if there's a trailing newline
tail -c 20 examples/teams/requirements.txt | od -cRepository: jentic/standard-agent
Length of output: 255
🌐 Web query:
latest version of botbuilder-core python package
💡 Result:
The latest botbuilder-core (Python) release is 4.17.1 — uploaded to PyPI on January 5, 2026. [1][2]
Sources:
[1] PyPI project page for botbuilder-core.
[2] piwheels release listing for botbuilder-core.
🌐 Web query:
cryptography python package latest version 2025
💡 Result:
The PyCA "cryptography" package latest release in 2025 was version 46.0.3, published on October 15, 2025. [1][2]
Sources:
- PyPI release details for cryptography. [1]
- cryptography changelog (46.0.0 release notes). [2]
🌐 Web query:
aiohttp python package latest stable version 2025
💡 Result:
The latest stable aiohttp release in 2025 was version 3.13.1 (released October 17, 2025). [1][2]
Sources:
- aiohttp changelog (3.13.1 entry). [1]
- PyPI release history (shows v3.13.3 uploaded Jan 3, 2026 — outside 2025). [2]
Add an upper bound to cryptography for safety.
The version ranges for Bot Framework packages and aiohttp are well-constrained. However:
Line 14: cryptography>=41.0.0 lacks an upper bound, which could break on a major version bump (latest is 46.0.3). Add <47.0.0 to constrain to compatible versions.
The dev dependencies without upper bounds (pytest, pytest-asyncio) are acceptable for development packages.
🤖 Prompt for AI Agents
In `@examples/teams/requirements.txt` around lines 1 - 18, The cryptography
dependency line currently reads "cryptography>=41.0.0" and lacks an upper bound;
update that requirement to include an upper bound (e.g., change to
"cryptography>=41.0.0,<47.0.0") so the cryptography package is constrained to a
safe major-version range.
| except Exception as exc: | ||
| logger.exception("teams_message_error", error=str(exc), user_id=user_id) | ||
| error_msg = "🚨 An unexpected error occurred. Our team has been notified." | ||
| await turn_context.send_activity(MessageFactory.text(error_msg)) | ||
|
|
There was a problem hiding this comment.
Potential NameError if exception occurs before user_id is assigned.
If an exception occurs before line 177 (e.g., if turn_context.activity is malformed), user_id will be undefined, causing a NameError on line 268.
🐛 Proposed fix
async def on_message_activity(self, turn_context: TurnContext):
"""Handle incoming messages with rate limiting and security."""
+ user_id = "unknown"
try:
# Get user ID for rate limiting
user_id = turn_context.activity.from_property.id if turn_context.activity.from_property else "unknown"🤖 Prompt for AI Agents
In `@examples/teams/teams_agent.py` around lines 267 - 271, The except block that
calls logger.exception("teams_message_error", error=str(exc), user_id=user_id)
can raise a NameError if user_id wasn't set earlier; update the except handler
in the teams_agent.py except Exception as exc block to resolve user_id safely
(e.g., derive it from turn_context.activity.from_property.id if present, or use
a fallback like None or "unknown") before passing it to logger.exception and
sending the error message; ensure you reference the same safe variable name
(user_id) when logging so the logger call can never reference an undefined
symbol.
| async def _handle_kill_command(self, turn_context: TurnContext): | ||
| """Handle the kill command.""" | ||
| self.runtime.current_agent = None | ||
| os.environ.pop("JENTIC_AGENT_API_KEY", None) | ||
| logger.warning("agent_killed") | ||
| await turn_context.send_activity( | ||
| MessageFactory.text("Agent killed. API key cleared; new requests will be rejected until reconfigured.") | ||
| ) |
There was a problem hiding this comment.
/kill command affects all users in shared deployment.
The /kill command clears the API key from the process environment (os.environ.pop), which affects all users of the bot instance. In a shared deployment, any user could disable the bot for everyone.
Consider:
- Adding authorization checks (e.g., only admins can run
/kill) - Making the API key per-conversation rather than global
- At minimum, documenting this behavior clearly
| async def authenticate_request(request: Request, app_credentials: MicrosoftAppCredentials) -> bool: | ||
| """Authenticate incoming requests from Teams.""" | ||
| try: | ||
| auth_header = request.headers.get("Authorization", "") | ||
| if not auth_header: | ||
| logger.warning("missing_auth_header") | ||
| return False | ||
|
|
||
| # Extract the token from the auth header | ||
| parts = auth_header.split(" ") | ||
| if len(parts) != 2 or parts[0].lower() != "bearer": | ||
| logger.warning("invalid_auth_header_format") | ||
| return False | ||
|
|
||
| token = parts[1] | ||
|
|
||
| if os.getenv("TEAMS_DEV_MODE", "").lower() == "true": | ||
| logger.warning("teams_dev_mode_enabled_skipping_auth") | ||
| return True | ||
|
|
||
| # Validate the JWT token using Bot Framework validation | ||
| # Note: This is a simplified implementation | ||
| # In production, implement proper JWT validation with claims verification | ||
| try: | ||
| if not token or len(token) < 50: | ||
| logger.warning("invalid_token_format") | ||
| return False | ||
|
|
||
| return True | ||
|
|
||
| except Exception as validation_exc: | ||
| logger.error("token_validation_failed", error=str(validation_exc)) | ||
| return False | ||
|
|
||
| except Exception as exc: | ||
| logger.error("auth_failed", error=str(exc)) | ||
| return False |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the teams_agent.py file
find . -name "teams_agent.py" -type fRepository: jentic/standard-agent
Length of output: 96
🏁 Script executed:
# Check the imports at the top of the file to see if JwtTokenValidation is imported
head -50 examples/teams/teams_agent.pyRepository: jentic/standard-agent
Length of output: 1238
🏁 Script executed:
# Examine the authenticate_request function more closely
sed -n '404,440p' examples/teams/teams_agent.pyRepository: jentic/standard-agent
Length of output: 1485
🏁 Script executed:
# Search for any JwtTokenValidation usage in the codebase
rg "JwtTokenValidation" --type pyRepository: jentic/standard-agent
Length of output: 118
🏁 Script executed:
# Check if app_credentials is used anywhere in the authenticate_request function
sed -n '404,440p' examples/teams/teams_agent.py | rg "app_credentials"Repository: jentic/standard-agent
Length of output: 164
🏁 Script executed:
# Also check the full imports section to see what's available
rg "from botframework" examples/teams/teams_agent.pyRepository: jentic/standard-agent
Length of output: 157
🌐 Web query:
botframework-connector JwtTokenValidation authenticate_activity Python documentation
💡 Result:
The Python Bot Framework connector exposes JwtTokenValidation.authenticate_request(...) (not authenticate_activity). See the API docs for the class and the authenticate_request signature and parameters. [1][2]
References
- JwtTokenValidation class (authenticate_request signature). [1]
- botframework-connector package (PyPI). [2]
Authentication is effectively a no-op — critical security vulnerability.
The authenticate_request function does not perform actual JWT token validation. It only checks that:
- An Authorization header exists
- It follows Bearer format
- The token is at least 50 characters
The app_credentials parameter is unused, and the comment on lines 425-426 acknowledges this is "simplified." However, this allows any request with a valid-looking header to pass authentication, potentially enabling unauthorized access.
Additionally, TEAMS_DEV_MODE=true bypasses authentication entirely, and JwtTokenValidation is imported but never used.
Implement proper JWT validation using the Bot Framework's JwtTokenValidation.authenticate_request() method instead of the stub length check:
claims = await JwtTokenValidation.authenticate_request(
activity,
auth_header,
app_credentials
)🧰 Tools
🪛 Ruff (0.14.11)
404-404: Unused function argument: app_credentials
(ARG001)
432-432: Consider moving this statement to an else block
(TRY300)
434-434: Do not catch blind exception: Exception
(BLE001)
435-435: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
438-438: Do not catch blind exception: Exception
(BLE001)
439-439: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
🤖 Prompt for AI Agents
In `@examples/teams/teams_agent.py` around lines 404 - 440, The
authenticate_request function currently does only a superficial token-length
check and ignores app_credentials; replace that stub with a real Bot Framework
JWT validation by parsing the incoming request body into an Activity (e.g.,
await request.json() -> Activity(...)) and calling await
JwtTokenValidation.authenticate_request(activity, auth_header, app_credentials);
return True when that call succeeds, catch and log JwtTokenValidation exceptions
and return False on failure, ensure the app_credentials argument is used, and if
you keep the TEAMS_DEV_MODE bypass preserve it only for explicit local dev use
with a clear log entry.
| except Exception as processing_exc: | ||
| logger.exception("activity_processing_failed", error=str(processing_exc)) | ||
| await turn_context.send_activity( | ||
| MessageFactory.text("Sorry, I encountered an error processing your request. Please try again.") | ||
| ) | ||
| return web.Response(status=200) |
There was a problem hiding this comment.
Potential NameError: turn_context may be undefined.
If an exception occurs on lines 499-505 (before turn_context is created on line 506), the error handler on line 515 will raise a NameError when trying to use turn_context.
🐛 Proposed fix
+ turn_context = None
try:
credentials = MicrosoftAppCredentials(
runtime.app_credentials.microsoft_app_id,
runtime.app_credentials.microsoft_app_password
)
connector_client = ConnectorClient(credentials, base_url=activity.service_url)
turn_context = TurnContext(bot, activity)
turn_context.turn_state[TurnContext.connector_client_key] = connector_client
await bot.on_turn(turn_context)
return web.Response(status=200)
except Exception as processing_exc:
logger.exception("activity_processing_failed", error=str(processing_exc))
- await turn_context.send_activity(
- MessageFactory.text("Sorry, I encountered an error processing your request. Please try again.")
- )
+ if turn_context:
+ await turn_context.send_activity(
+ MessageFactory.text("Sorry, I encountered an error processing your request. Please try again.")
+ )
return web.Response(status=200)🤖 Prompt for AI Agents
In `@examples/teams/teams_agent.py` around lines 513 - 518, The exception handler
references turn_context which may not be defined if the error occurs before it's
created; update the except block in the same scope to safely handle that by
checking for the variable (e.g., if "turn_context" in locals() and turn_context
is not None) before calling turn_context.send_activity, otherwise fall back to a
safe alternative such as using the bot adapter to send a proactive message or
simply logging the error and returning web.Response(status=200); ensure the
check uses the exact symbol turn_context and that any fallback uses existing
adapter/logger symbols available in the surrounding function.
Closes #88
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.