LangChain Integration
Securely relay OTPs to your LangChain agents. Enable your agents to complete verification flows without direct access to SMS or email.
Note: The Python SDK is coming soon. This guide shows the integration pattern using the TypeScript SDK. The Python SDK will have a similar API.
Overview
Agent OTP helps your LangChain agents receive verification codes securely:
- Agent requests an OTP when it needs to complete a verification
- User approves which OTP to share
- OTP is encrypted and delivered to the agent
- OTP is auto-deleted after consumption
TypeScript Example
Here's how to create an OTP-enabled tool for LangChain.js:
import {
AgentOTPClient,
generateKeyPair,
exportPublicKey,
} from '@orrisai/agent-otp-sdk';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
const otp = new AgentOTPClient({
apiKey: process.env.AGENT_OTP_API_KEY!,
});
// Store key pair securely (e.g., in environment or secure storage)
const { publicKey, privateKey } = await generateKeyPair();
const signUpTool = new DynamicStructuredTool({
name: 'sign_up_for_service',
description: 'Sign up for a service that requires email verification',
schema: z.object({
email: z.string().email(),
serviceName: z.string(),
serviceUrl: z.string().url(),
}),
func: async ({ email, serviceName, serviceUrl }) => {
// Step 1: Start the sign-up process (your implementation)
await startSignUp(serviceUrl, email);
// Step 2: Request OTP from Agent OTP
const request = await otp.requestOTP({
reason: `Sign up verification for ${serviceName}`,
expectedSender: serviceName,
filter: {
sources: ['email'],
senderPattern: `*@${new URL(serviceUrl).hostname}`,
},
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
timeout: 120000, // 2 minutes
});
if (request.status !== 'otp_received') {
return `Could not get verification code: ${request.status}`;
}
// Step 3: Consume the OTP
const { code } = await otp.consumeOTP(request.id, privateKey);
// Step 4: Complete verification (your implementation)
await completeVerification(serviceUrl, code);
return `Successfully signed up for ${serviceName} with ${email}`;
},
});Python Example (Coming Soon)
from agent_otp import AgentOTPClient, generate_key_pair, export_public_key
from langchain.tools import BaseTool
otp = AgentOTPClient(api_key="ak_live_xxxx")
# Generate encryption keys
public_key, private_key = generate_key_pair()
class SignUpTool(BaseTool):
name = "sign_up_for_service"
description = "Sign up for a service that requires email verification"
def _run(self, email: str, service_name: str, service_url: str) -> str:
# Start sign-up process
start_sign_up(service_url, email)
# Request OTP
request = otp.request_otp(
reason=f"Sign up verification for {service_name}",
expected_sender=service_name,
filter={
"sources": ["email"],
"sender_pattern": f"*@{service_url.split('/')[2]}"
},
public_key=export_public_key(public_key),
wait_for_otp=True,
timeout=120
)
if request.status != "otp_received":
return f"Could not get verification code: {request.status}"
# Consume the OTP
result = otp.consume_otp(request.id, private_key)
# Complete verification
complete_verification(service_url, result.code)
return f"Successfully signed up for {service_name}"Handling OTP States
Your agent should handle different OTP request states:
const handleOTPRequest = async (reason: string, sender: string) => {
const request = await otp.requestOTP({
reason,
expectedSender: sender,
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
timeout: 120000,
});
switch (request.status) {
case 'otp_received':
const { code } = await otp.consumeOTP(request.id, privateKey);
return { success: true, code };
case 'pending_approval':
return { success: false, reason: 'Waiting for user approval' };
case 'approved':
return { success: false, reason: 'Waiting for OTP to arrive' };
case 'denied':
return { success: false, reason: 'User denied the request' };
case 'expired':
return { success: false, reason: 'Request timed out' };
default:
return { success: false, reason: `Unexpected status: ${request.status}` };
}
};Error Handling
import {
OTPNotFoundError,
OTPExpiredError,
OTPAlreadyConsumedError,
OTPApprovalDeniedError,
DecryptionError,
RateLimitError,
} from '@orrisai/agent-otp-sdk';
const signUpTool = new DynamicStructuredTool({
name: 'sign_up_with_otp',
description: 'Sign up with OTP verification',
schema: z.object({ /* ... */ }),
func: async (params) => {
try {
const request = await otp.requestOTP({
reason: 'Sign up verification',
publicKey: await exportPublicKey(publicKey),
waitForOTP: true,
});
if (request.status === 'otp_received') {
const { code } = await otp.consumeOTP(request.id, privateKey);
return `Received code: ${code}`;
}
return `OTP request status: ${request.status}`;
} catch (error) {
if (error instanceof OTPApprovalDeniedError) {
return 'User denied the OTP request';
}
if (error instanceof OTPExpiredError) {
return 'OTP request expired - please try again';
}
if (error instanceof OTPAlreadyConsumedError) {
return 'OTP was already consumed';
}
if (error instanceof DecryptionError) {
return 'Failed to decrypt OTP - check your keys';
}
if (error instanceof RateLimitError) {
return `Rate limited - retry in ${error.retryAfter}s`;
}
throw error;
}
},
});LangGraph Integration
For complex workflows, use LangGraph with OTP nodes:
import { StateGraph, END } from '@langchain/langgraph';
interface WorkflowState {
action: string;
otpRequestId?: string;
otpStatus?: string;
code?: string;
result?: string;
}
const requestOTPNode = async (state: WorkflowState) => {
const request = await otp.requestOTP({
reason: `Verification for ${state.action}`,
publicKey: await exportPublicKey(publicKey),
waitForOTP: false,
});
return {
...state,
otpRequestId: request.id,
otpStatus: request.status,
};
};
const checkStatusNode = async (state: WorkflowState) => {
const status = await otp.getOTPStatus(state.otpRequestId!);
return { ...state, otpStatus: status.status };
};
const consumeOTPNode = async (state: WorkflowState) => {
const { code } = await otp.consumeOTP(state.otpRequestId!, privateKey);
return { ...state, code };
};
const graph = new StateGraph<WorkflowState>({
channels: {
action: { value: null },
otpRequestId: { value: null },
otpStatus: { value: null },
code: { value: null },
result: { value: null },
},
});
graph.addNode('request_otp', requestOTPNode);
graph.addNode('check_status', checkStatusNode);
graph.addNode('consume_otp', consumeOTPNode);
graph.setEntryPoint('request_otp');
graph.addConditionalEdges('request_otp', (state) =>
state.otpStatus === 'otp_received' ? 'consume_otp' : 'check_status'
);
graph.addConditionalEdges('check_status', (state) =>
state.otpStatus === 'otp_received' ? 'consume_otp' : 'check_status'
);
graph.addEdge('consume_otp', END);