Skip to content

Commit bc25f1c

Browse files
author
safenestdev
committed
feat: add fraud detection, safety extended, multi-endpoint analysis, and video analysis support
1 parent 3097785 commit bc25f1c

4 files changed

Lines changed: 593 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "tuteliq"
7-
version = "2.2.0"
7+
version = "2.3.0"
88
description = "Official Python SDK for Tuteliq - AI-powered child safety API"
99
readme = "README.md"
1010
license = "MIT"

tuteliq/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
ConsentType,
2323
ConsentStatus,
2424
AuditAction,
25+
Detection,
26+
Language,
27+
LanguageStatus,
28+
Tier,
29+
SUPPORTED_LANGUAGES,
2530
# Input types
2631
AnalysisContext,
2732
DetectBullyingInput,
@@ -38,6 +43,8 @@
3843
RectifyDataInput,
3944
CreateWebhookInput,
4045
UpdateWebhookInput,
46+
DetectionInput,
47+
AnalyseMultiInput,
4148
# Result types
4249
BullyingResult,
4350
GroomingResult,
@@ -53,6 +60,15 @@
5360
VoiceAnalysisResult,
5461
VisionResult,
5562
ImageAnalysisResult,
63+
# Detection result types (Fraud + Safety Extended)
64+
DetectionCategory,
65+
DetectionEvidence,
66+
DetectionResult,
67+
AgeCalibration,
68+
AnalyseMultiResult,
69+
AnalyseMultiSummary,
70+
VideoAnalysisResult,
71+
VideoSafetyFinding,
5672
# Webhook types
5773
Webhook,
5874
WebhookListResult,
@@ -106,7 +122,7 @@
106122
TierAccessError,
107123
)
108124

109-
__version__ = "2.2.0"
125+
__version__ = "2.3.0"
110126
__all__ = [
111127
# Client
112128
"Tuteliq",
@@ -145,6 +161,22 @@
145161
"ActionPlanResult",
146162
"ReportResult",
147163
"Usage",
164+
# Detection types (Fraud + Safety Extended)
165+
"Detection",
166+
"DetectionCategory",
167+
"DetectionEvidence",
168+
"DetectionInput",
169+
"DetectionResult",
170+
"AgeCalibration",
171+
"AnalyseMultiInput",
172+
"AnalyseMultiResult",
173+
"AnalyseMultiSummary",
174+
"Language",
175+
"LanguageStatus",
176+
"SUPPORTED_LANGUAGES",
177+
"Tier",
178+
"VideoAnalysisResult",
179+
"VideoSafetyFinding",
148180
# Voice/Image types
149181
"TranscriptionSegment",
150182
"TranscriptionResult",

tuteliq/client.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
WebhookListResult,
7272
ReportResult,
7373
Usage,
74+
DetectionInput,
75+
DetectionResult,
76+
AnalyseMultiInput,
77+
AnalyseMultiResult,
78+
VideoAnalysisResult,
7479
)
7580

7681

@@ -106,6 +111,42 @@ def _resolve_platform(platform: Optional[str] = None) -> str:
106111
return f"{platform} - {Tuteliq._SDK_IDENTIFIER}"
107112
return Tuteliq._SDK_IDENTIFIER
108113

114+
@staticmethod
115+
def _resolve_detection_input(
116+
content_or_input: Union[str, DetectionInput],
117+
context: Optional[AnalysisContext],
118+
include_evidence: bool,
119+
external_id: Optional[str],
120+
customer_id: Optional[str],
121+
metadata: Optional[dict[str, Any]],
122+
) -> DetectionInput:
123+
"""Resolve string or DetectionInput to DetectionInput."""
124+
if isinstance(content_or_input, str):
125+
return DetectionInput(
126+
content=content_or_input,
127+
context=context,
128+
include_evidence=include_evidence,
129+
external_id=external_id,
130+
customer_id=customer_id,
131+
metadata=metadata,
132+
)
133+
return content_or_input
134+
135+
def _build_detection_body(self, input_data: DetectionInput) -> dict[str, Any]:
136+
"""Build request body for unified detection endpoints."""
137+
ctx = input_data.context.to_dict() if input_data.context else {}
138+
ctx["platform"] = self._resolve_platform(ctx.get("platform"))
139+
body: dict[str, Any] = {"text": input_data.content, "context": ctx}
140+
if input_data.include_evidence:
141+
body["include_evidence"] = True
142+
if input_data.external_id:
143+
body["external_id"] = input_data.external_id
144+
if input_data.customer_id:
145+
body["customer_id"] = input_data.customer_id
146+
if input_data.metadata:
147+
body["metadata"] = input_data.metadata
148+
return body
149+
109150
def __init__(
110151
self,
111152
api_key: str,
@@ -824,6 +865,233 @@ async def analyze_image(
824865
data = await self._multipart_request("/api/v1/safety/image", fields, files)
825866
return ImageAnalysisResult.from_dict(data)
826867

868+
# =========================================================================
869+
# Fraud Detection
870+
# =========================================================================
871+
872+
async def detect_social_engineering(
873+
self,
874+
content_or_input: Union[str, DetectionInput],
875+
*,
876+
context: Optional[AnalysisContext] = None,
877+
include_evidence: bool = False,
878+
external_id: Optional[str] = None,
879+
customer_id: Optional[str] = None,
880+
metadata: Optional[dict[str, Any]] = None,
881+
) -> DetectionResult:
882+
"""Detect social engineering tactics."""
883+
input_data = self._resolve_detection_input(
884+
content_or_input, context, include_evidence, external_id, customer_id, metadata
885+
)
886+
data = await self._request("POST", "/api/v1/fraud/social-engineering", self._build_detection_body(input_data))
887+
return DetectionResult.from_dict(data)
888+
889+
async def detect_app_fraud(
890+
self,
891+
content_or_input: Union[str, DetectionInput],
892+
*,
893+
context: Optional[AnalysisContext] = None,
894+
include_evidence: bool = False,
895+
external_id: Optional[str] = None,
896+
customer_id: Optional[str] = None,
897+
metadata: Optional[dict[str, Any]] = None,
898+
) -> DetectionResult:
899+
"""Detect app-based fraud patterns."""
900+
input_data = self._resolve_detection_input(
901+
content_or_input, context, include_evidence, external_id, customer_id, metadata
902+
)
903+
data = await self._request("POST", "/api/v1/fraud/app-fraud", self._build_detection_body(input_data))
904+
return DetectionResult.from_dict(data)
905+
906+
async def detect_romance_scam(
907+
self,
908+
content_or_input: Union[str, DetectionInput],
909+
*,
910+
context: Optional[AnalysisContext] = None,
911+
include_evidence: bool = False,
912+
external_id: Optional[str] = None,
913+
customer_id: Optional[str] = None,
914+
metadata: Optional[dict[str, Any]] = None,
915+
) -> DetectionResult:
916+
"""Detect romance scam patterns."""
917+
input_data = self._resolve_detection_input(
918+
content_or_input, context, include_evidence, external_id, customer_id, metadata
919+
)
920+
data = await self._request("POST", "/api/v1/fraud/romance-scam", self._build_detection_body(input_data))
921+
return DetectionResult.from_dict(data)
922+
923+
async def detect_mule_recruitment(
924+
self,
925+
content_or_input: Union[str, DetectionInput],
926+
*,
927+
context: Optional[AnalysisContext] = None,
928+
include_evidence: bool = False,
929+
external_id: Optional[str] = None,
930+
customer_id: Optional[str] = None,
931+
metadata: Optional[dict[str, Any]] = None,
932+
) -> DetectionResult:
933+
"""Detect money mule recruitment."""
934+
input_data = self._resolve_detection_input(
935+
content_or_input, context, include_evidence, external_id, customer_id, metadata
936+
)
937+
data = await self._request("POST", "/api/v1/fraud/mule-recruitment", self._build_detection_body(input_data))
938+
return DetectionResult.from_dict(data)
939+
940+
# =========================================================================
941+
# Safety Extended
942+
# =========================================================================
943+
944+
async def detect_gambling_harm(
945+
self,
946+
content_or_input: Union[str, DetectionInput],
947+
*,
948+
context: Optional[AnalysisContext] = None,
949+
include_evidence: bool = False,
950+
external_id: Optional[str] = None,
951+
customer_id: Optional[str] = None,
952+
metadata: Optional[dict[str, Any]] = None,
953+
) -> DetectionResult:
954+
"""Detect gambling harm indicators."""
955+
input_data = self._resolve_detection_input(
956+
content_or_input, context, include_evidence, external_id, customer_id, metadata
957+
)
958+
data = await self._request("POST", "/api/v1/safety/gambling-harm", self._build_detection_body(input_data))
959+
return DetectionResult.from_dict(data)
960+
961+
async def detect_coercive_control(
962+
self,
963+
content_or_input: Union[str, DetectionInput],
964+
*,
965+
context: Optional[AnalysisContext] = None,
966+
include_evidence: bool = False,
967+
external_id: Optional[str] = None,
968+
customer_id: Optional[str] = None,
969+
metadata: Optional[dict[str, Any]] = None,
970+
) -> DetectionResult:
971+
"""Detect coercive control patterns."""
972+
input_data = self._resolve_detection_input(
973+
content_or_input, context, include_evidence, external_id, customer_id, metadata
974+
)
975+
data = await self._request("POST", "/api/v1/safety/coercive-control", self._build_detection_body(input_data))
976+
return DetectionResult.from_dict(data)
977+
978+
async def detect_vulnerability_exploitation(
979+
self,
980+
content_or_input: Union[str, DetectionInput],
981+
*,
982+
context: Optional[AnalysisContext] = None,
983+
include_evidence: bool = False,
984+
external_id: Optional[str] = None,
985+
customer_id: Optional[str] = None,
986+
metadata: Optional[dict[str, Any]] = None,
987+
) -> DetectionResult:
988+
"""Detect vulnerability exploitation with cross-endpoint modifier."""
989+
input_data = self._resolve_detection_input(
990+
content_or_input, context, include_evidence, external_id, customer_id, metadata
991+
)
992+
data = await self._request("POST", "/api/v1/safety/vulnerability-exploitation", self._build_detection_body(input_data))
993+
return DetectionResult.from_dict(data)
994+
995+
async def detect_radicalisation(
996+
self,
997+
content_or_input: Union[str, DetectionInput],
998+
*,
999+
context: Optional[AnalysisContext] = None,
1000+
include_evidence: bool = False,
1001+
external_id: Optional[str] = None,
1002+
customer_id: Optional[str] = None,
1003+
metadata: Optional[dict[str, Any]] = None,
1004+
) -> DetectionResult:
1005+
"""Detect radicalisation indicators."""
1006+
input_data = self._resolve_detection_input(
1007+
content_or_input, context, include_evidence, external_id, customer_id, metadata
1008+
)
1009+
data = await self._request("POST", "/api/v1/safety/radicalisation", self._build_detection_body(input_data))
1010+
return DetectionResult.from_dict(data)
1011+
1012+
# =========================================================================
1013+
# Multi-Endpoint Analysis
1014+
# =========================================================================
1015+
1016+
async def analyse_multi(
1017+
self, input_data: AnalyseMultiInput
1018+
) -> AnalyseMultiResult:
1019+
"""Run multiple detection endpoints on a single piece of content.
1020+
1021+
Args:
1022+
input_data: AnalyseMultiInput with content and detections list.
1023+
1024+
Returns:
1025+
AnalyseMultiResult with per-endpoint results and summary.
1026+
"""
1027+
ctx = input_data.context.to_dict() if input_data.context else {}
1028+
ctx["platform"] = self._resolve_platform(ctx.get("platform"))
1029+
body: dict[str, Any] = {
1030+
"text": input_data.content,
1031+
"endpoints": input_data.detections,
1032+
"context": ctx,
1033+
}
1034+
if input_data.include_evidence:
1035+
body["options"] = {"include_evidence": True}
1036+
if input_data.external_id:
1037+
body["external_id"] = input_data.external_id
1038+
if input_data.customer_id:
1039+
body["customer_id"] = input_data.customer_id
1040+
if input_data.metadata:
1041+
body["metadata"] = input_data.metadata
1042+
1043+
data = await self._request("POST", "/api/v1/analyse/multi", body)
1044+
return AnalyseMultiResult.from_dict(data)
1045+
1046+
# =========================================================================
1047+
# Video Analysis
1048+
# =========================================================================
1049+
1050+
async def analyze_video(
1051+
self,
1052+
file: bytes,
1053+
filename: str,
1054+
*,
1055+
file_id: Optional[str] = None,
1056+
external_id: Optional[str] = None,
1057+
customer_id: Optional[str] = None,
1058+
metadata: Optional[dict[str, Any]] = None,
1059+
age_group: Optional[str] = None,
1060+
platform: Optional[str] = None,
1061+
) -> VideoAnalysisResult:
1062+
"""Analyze video content for safety concerns.
1063+
1064+
Args:
1065+
file: Raw video file bytes.
1066+
filename: Original filename (e.g. "clip.mp4").
1067+
file_id: Optional file identifier.
1068+
external_id: Your identifier for correlation.
1069+
customer_id: Customer identifier.
1070+
metadata: Custom metadata.
1071+
age_group: Age group context.
1072+
platform: Platform identifier.
1073+
1074+
Returns:
1075+
VideoAnalysisResult with frame findings and overall risk.
1076+
"""
1077+
fields: dict[str, Any] = {
1078+
"platform": self._resolve_platform(platform),
1079+
}
1080+
if file_id:
1081+
fields["file_id"] = file_id
1082+
if external_id:
1083+
fields["external_id"] = external_id
1084+
if customer_id:
1085+
fields["customer_id"] = customer_id
1086+
if metadata:
1087+
fields["metadata"] = json.dumps(metadata)
1088+
if age_group:
1089+
fields["age_group"] = age_group
1090+
1091+
files = {"file": (filename, file, "application/octet-stream")}
1092+
data = await self._multipart_request("/api/v1/safety/video", fields, files)
1093+
return VideoAnalysisResult.from_dict(data)
1094+
8271095
# =========================================================================
8281096
# Voice Streaming
8291097
# =========================================================================

0 commit comments

Comments
 (0)