deeptutor-partners 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. deeptutor/api/routers/_partners_channel_schema.py +176 -0
  2. deeptutor/api/routers/partner_groups.py +534 -0
  3. deeptutor/api/routers/partners.py +2111 -0
  4. deeptutor/capabilities/handoff/__init__.py +5 -0
  5. deeptutor/capabilities/handoff/capability.py +512 -0
  6. deeptutor/capabilities/partner_authoring/__init__.py +13 -0
  7. deeptutor/capabilities/partner_authoring/binding.py +100 -0
  8. deeptutor/capabilities/partner_authoring/capability.py +71 -0
  9. deeptutor/capabilities/partner_authoring/prompts/en/heuristic.md +1 -0
  10. deeptutor/capabilities/partner_authoring/prompts/en/system.md +9 -0
  11. deeptutor/capabilities/partner_authoring/prompts/zh/heuristic.md +1 -0
  12. deeptutor/capabilities/partner_authoring/prompts/zh/system.md +9 -0
  13. deeptutor/capabilities/partner_authoring/tools.py +83 -0
  14. deeptutor/capabilities/partner_group/__init__.py +13 -0
  15. deeptutor/capabilities/partner_group/capability.py +229 -0
  16. deeptutor/capabilities/partner_group/prompts/en/invoke_other.md +1 -0
  17. deeptutor/capabilities/partner_group/prompts/en/system.md +3 -0
  18. deeptutor/capabilities/partner_group/prompts/zh/invoke_other.md +1 -0
  19. deeptutor/capabilities/partner_group/prompts/zh/system.md +3 -0
  20. deeptutor/capabilities/partner_group/tools.py +162 -0
  21. deeptutor/partners/__init__.py +11 -0
  22. deeptutor/partners/bus/__init__.py +6 -0
  23. deeptutor/partners/bus/events.py +41 -0
  24. deeptutor/partners/bus/queue.py +63 -0
  25. deeptutor/partners/channels/__init__.py +6 -0
  26. deeptutor/partners/channels/base.py +276 -0
  27. deeptutor/partners/channels/dingtalk.py +550 -0
  28. deeptutor/partners/channels/discord.py +517 -0
  29. deeptutor/partners/channels/email.py +470 -0
  30. deeptutor/partners/channels/feishu.py +2459 -0
  31. deeptutor/partners/channels/lark_http.py +79 -0
  32. deeptutor/partners/channels/manager.py +518 -0
  33. deeptutor/partners/channels/matrix.py +844 -0
  34. deeptutor/partners/channels/mattermost.py +450 -0
  35. deeptutor/partners/channels/mochat.py +1076 -0
  36. deeptutor/partners/channels/msteams.py +847 -0
  37. deeptutor/partners/channels/napcat.py +594 -0
  38. deeptutor/partners/channels/qq.py +203 -0
  39. deeptutor/partners/channels/registry.py +97 -0
  40. deeptutor/partners/channels/slack.py +333 -0
  41. deeptutor/partners/channels/telegram.py +1157 -0
  42. deeptutor/partners/channels/wecom.py +398 -0
  43. deeptutor/partners/channels/weixin.py +1445 -0
  44. deeptutor/partners/channels/weixin_qr.py +202 -0
  45. deeptutor/partners/channels/whatsapp.py +210 -0
  46. deeptutor/partners/channels/zulip.py +859 -0
  47. deeptutor/partners/config/__init__.py +26 -0
  48. deeptutor/partners/config/paths.py +89 -0
  49. deeptutor/partners/config/schema.py +50 -0
  50. deeptutor/partners/helpers.py +109 -0
  51. deeptutor/partners/network.py +101 -0
  52. deeptutor/partners/transcription.py +61 -0
  53. deeptutor/services/partner_groups/__init__.py +18 -0
  54. deeptutor/services/partner_groups/consultation.py +40 -0
  55. deeptutor/services/partner_groups/manager.py +1638 -0
  56. deeptutor/services/partner_groups/memory.py +204 -0
  57. deeptutor/services/partner_groups/models.py +115 -0
  58. deeptutor/services/partner_groups/modes.py +195 -0
  59. deeptutor/services/partner_groups/store.py +442 -0
  60. deeptutor/services/partners/__init__.py +28 -0
  61. deeptutor/services/partners/channel_onboarding.py +552 -0
  62. deeptutor/services/partners/channel_state_migration.py +127 -0
  63. deeptutor/services/partners/commands.py +497 -0
  64. deeptutor/services/partners/drafts.py +145 -0
  65. deeptutor/services/partners/interaction.py +189 -0
  66. deeptutor/services/partners/links.py +197 -0
  67. deeptutor/services/partners/manager.py +1877 -0
  68. deeptutor/services/partners/model_runtime.py +36 -0
  69. deeptutor/services/partners/module_service.py +15 -0
  70. deeptutor/services/partners/runtime.py +1174 -0
  71. deeptutor/services/partners/runtime_status.py +133 -0
  72. deeptutor/services/partners/scope.py +43 -0
  73. deeptutor/services/partners/sessions.py +462 -0
  74. deeptutor/services/partners/web_continuity.py +175 -0
  75. deeptutor/services/partners/weixin_onboarding.py +324 -0
  76. deeptutor/services/partners/workspace.py +451 -0
  77. deeptutor/services/partners/workspace_binding.py +48 -0
  78. deeptutor/tools/partner_memory.py +315 -0
  79. deeptutor_cli/partner.py +103 -0
  80. deeptutor_partners-1.7.0.dist-info/METADATA +24 -0
  81. deeptutor_partners-1.7.0.dist-info/RECORD +83 -0
  82. deeptutor_partners-1.7.0.dist-info/WHEEL +5 -0
  83. deeptutor_partners-1.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,176 @@
1
+ """Channel-schema introspection — bridges Pydantic channel configs to the Web UI.
2
+
3
+ Used by ``GET /api/partners/channels/schema`` so the front-end can render
4
+ generic forms for ANY channel (built-in or plugin) without hard-coding fields.
5
+
6
+ Why live here vs inside ``deeptutor.partners.channels``?
7
+ * This is an API-shaping concern (JSON Schema flattening, secret-field
8
+ detection) — keeping it next to the route avoids polluting the runtime
9
+ channel package with HTTP-specific helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import inspect
15
+ from typing import Any
16
+
17
+ from pydantic import BaseModel
18
+
19
+ from deeptutor.services.partners.manager import _is_secret_field
20
+
21
+
22
+ def resolve_config_model(channel_cls: type) -> type[BaseModel] | None:
23
+ """Find the Pydantic config model paired with ``channel_cls``.
24
+
25
+ Convention every built-in channel follows: ``XxxChannel`` lives in the
26
+ same module as ``XxxConfig`` (e.g. ``TelegramChannel`` ↔ ``TelegramConfig``).
27
+ Falls back to "any ``*Config`` BaseModel in the module".
28
+ """
29
+ module = inspect.getmodule(channel_cls)
30
+ if module is None:
31
+ return None
32
+
33
+ expected = channel_cls.__name__.replace("Channel", "") + "Config"
34
+ candidate = getattr(module, expected, None)
35
+ if isinstance(candidate, type) and issubclass(candidate, BaseModel):
36
+ return candidate
37
+
38
+ for _, obj in inspect.getmembers(module):
39
+ if (
40
+ isinstance(obj, type)
41
+ and obj is not BaseModel
42
+ and issubclass(obj, BaseModel)
43
+ and obj.__name__.endswith("Config")
44
+ ):
45
+ return obj
46
+ return None
47
+
48
+
49
+ def inline_refs(schema: dict[str, Any]) -> dict[str, Any]:
50
+ """Flatten Pydantic's ``$defs`` / ``$ref`` so the front-end doesn't need a resolver.
51
+
52
+ Nested model fields (e.g. ``slack.dm: SlackDMConfig``) become inline
53
+ ``type: object`` subtrees with their own ``properties``.
54
+ """
55
+ defs: dict[str, Any] = dict(schema.get("$defs", {}))
56
+
57
+ def _walk(node: Any) -> Any:
58
+ if isinstance(node, dict):
59
+ ref = node.get("$ref")
60
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
61
+ ref_name = ref.rsplit("/", 1)[-1]
62
+ resolved = defs.get(ref_name, {})
63
+ merged = {**resolved}
64
+ # Allow per-field overrides (description, default) from the ref site.
65
+ for k, v in node.items():
66
+ if k != "$ref":
67
+ merged[k] = v
68
+ return _walk(merged)
69
+ return {k: _walk(v) for k, v in node.items()}
70
+ if isinstance(node, list):
71
+ return [_walk(item) for item in node]
72
+ return node
73
+
74
+ out = _walk(schema)
75
+ if isinstance(out, dict):
76
+ out.pop("$defs", None)
77
+ return out
78
+
79
+
80
+ def _schema_accepts_string(prop_schema: dict[str, Any]) -> bool:
81
+ """True iff the JSON-Schema fragment can hold a string value.
82
+
83
+ Used to filter out booleans/integers/arrays whose name happens to contain
84
+ a secret-looking substring (e.g. ``user_token_read_only: bool``).
85
+ """
86
+ t = prop_schema.get("type")
87
+ if t == "string":
88
+ return True
89
+ if isinstance(t, list) and "string" in t:
90
+ return True
91
+ for variant in prop_schema.get("anyOf", []):
92
+ if isinstance(variant, dict) and variant.get("type") == "string":
93
+ return True
94
+ return False
95
+
96
+
97
+ def collect_secret_fields(schema: dict[str, Any], prefix: str = "") -> list[str]:
98
+ """Return dot-paths for every string-typed property whose name hints at a secret.
99
+
100
+ e.g. ``["token"]`` for telegram, ``["imap_password", "smtp_password"]``
101
+ for email, ``["bot_token", "app_token"]`` for slack. A field like
102
+ ``user_token_read_only: bool`` is intentionally skipped.
103
+ """
104
+ paths: list[str] = []
105
+ properties = schema.get("properties") if isinstance(schema, dict) else None
106
+ if not isinstance(properties, dict):
107
+ return paths
108
+
109
+ for prop_name, prop_schema in properties.items():
110
+ if not isinstance(prop_schema, dict):
111
+ continue
112
+ full = f"{prefix}{prop_name}" if not prefix else f"{prefix}.{prop_name}"
113
+ if _is_secret_field(prop_name) and _schema_accepts_string(prop_schema):
114
+ paths.append(full)
115
+ if prop_schema.get("type") == "object":
116
+ paths.extend(collect_secret_fields(prop_schema, prefix=full))
117
+ return paths
118
+
119
+
120
+ def channel_schema_payload(channel_cls: type) -> dict[str, Any] | None:
121
+ """Build the per-channel schema payload, or ``None`` if no config model found."""
122
+ model = resolve_config_model(channel_cls)
123
+ if model is None:
124
+ return None
125
+
126
+ # by_alias=False → property names match Python field names (snake_case),
127
+ # which is exactly the shape we persist in ``config.yaml`` and what every
128
+ # channel's ``__init__`` expects when ``model_validate(dict)`` is called.
129
+ # The pydantic Base config has populate_by_name=True so the runtime still
130
+ # accepts both forms; we standardise on snake_case for the wire schema.
131
+ raw = model.model_json_schema(by_alias=False)
132
+ flat = inline_refs(raw)
133
+ secret_fields = collect_secret_fields(flat)
134
+
135
+ try:
136
+ default_config = model().model_dump(mode="json", by_alias=False)
137
+ except Exception:
138
+ default_config = {}
139
+
140
+ return {
141
+ "name": getattr(channel_cls, "name", channel_cls.__name__),
142
+ "display_name": getattr(channel_cls, "display_name", channel_cls.__name__),
143
+ "default_config": default_config,
144
+ "secret_fields": secret_fields,
145
+ "json_schema": flat,
146
+ }
147
+
148
+
149
+ def all_channel_schemas() -> dict[str, dict[str, Any]]:
150
+ """Build the schema dict for every discovered channel (built-in + plugins).
151
+
152
+ Channels whose module failed to import (missing optional dependency)
153
+ still appear, marked ``available: False`` with the import error — the UI
154
+ shows them grayed out instead of silently dropping them.
155
+ """
156
+ from deeptutor.partners.channels.registry import discover_all_with_errors
157
+
158
+ channels, errors = discover_all_with_errors()
159
+
160
+ out: dict[str, dict[str, Any]] = {}
161
+ for name, cls in channels.items():
162
+ payload = channel_schema_payload(cls)
163
+ if payload is not None:
164
+ payload["available"] = True
165
+ out[name] = payload
166
+ for name, reason in errors.items():
167
+ out[name] = {
168
+ "name": name,
169
+ "display_name": name.title(),
170
+ "available": False,
171
+ "unavailable_reason": reason,
172
+ "default_config": {"enabled": False},
173
+ "secret_fields": [],
174
+ "json_schema": None,
175
+ }
176
+ return out