decodingtrust-agent-sdk 0.1.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.
- agent/__init__.py +30 -0
- agent/claudesdk/__init__.py +8 -0
- agent/claudesdk/example.py +221 -0
- agent/claudesdk/src/__init__.py +8 -0
- agent/claudesdk/src/agent.py +400 -0
- agent/claudesdk/src/mcp_proxy.py +409 -0
- agent/claudesdk/src/utils.py +420 -0
- agent/googleadk/__init__.py +15 -0
- agent/googleadk/example.py +237 -0
- agent/googleadk/src/__init__.py +12 -0
- agent/googleadk/src/agent.py +401 -0
- agent/googleadk/src/mcp_wrapper.py +163 -0
- agent/googleadk/src/utils.py +602 -0
- agent/langchain/__init__.py +8 -0
- agent/langchain/example.py +213 -0
- agent/langchain/src/__init__.py +8 -0
- agent/langchain/src/agent.py +645 -0
- agent/langchain/src/utils.py +433 -0
- agent/openaisdk/__init__.py +17 -0
- agent/openaisdk/example.py +228 -0
- agent/openaisdk/src/__init__.py +12 -0
- agent/openaisdk/src/agent.py +491 -0
- agent/openaisdk/src/agent_wrapper.py +143 -0
- agent/openaisdk/src/mcp_wrapper.py +395 -0
- agent/openaisdk/src/utils.py +493 -0
- agent/openclaw/__init__.py +10 -0
- agent/openclaw/example.py +251 -0
- agent/openclaw/src/__init__.py +14 -0
- agent/openclaw/src/agent.py +930 -0
- agent/openclaw/src/helpers/__init__.py +1 -0
- agent/openclaw/src/helpers/auth_helpers.py +55 -0
- agent/openclaw/src/mcp_proxy.py +564 -0
- agent/openclaw/src/plugin_generator.py +231 -0
- agent/openclaw/src/utils.py +341 -0
- agent/pocketflow/__init__.py +18 -0
- agent/pocketflow/example.py +221 -0
- agent/pocketflow/prompts/react_agent.py +46 -0
- agent/pocketflow/src/__init__.py +6 -0
- agent/pocketflow/src/agent.py +507 -0
- agent/pocketflow/src/agent_wrapper.py +159 -0
- agent/pocketflow/src/async_helper.py +92 -0
- agent/pocketflow/src/mcp_react_agent.py +279 -0
- agent/pocketflow/src/native_agent.py +74 -0
- agent/pocketflow/src/nodes.py +467 -0
- benchmark/__init__.py +0 -0
- benchmark/browser/benign.jsonl +34 -0
- benchmark/browser/direct.jsonl +85 -0
- benchmark/browser/indirect.jsonl +82 -0
- benchmark/code/benign.jsonl +0 -0
- benchmark/code/direct.jsonl +121 -0
- benchmark/code/indirect.jsonl +165 -0
- benchmark/crm/benign.jsonl +165 -0
- benchmark/crm/direct.jsonl +90 -0
- benchmark/crm/indirect.jsonl +150 -0
- benchmark/customer-service/benign.jsonl +160 -0
- benchmark/customer-service/direct.jsonl +100 -0
- benchmark/customer-service/indirect.jsonl +101 -0
- benchmark/finance/benign.jsonl +0 -0
- benchmark/finance/direct.jsonl +200 -0
- benchmark/finance/indirect.jsonl +200 -0
- benchmark/legal/benign.jsonl +0 -0
- benchmark/legal/direct.jsonl +200 -0
- benchmark/legal/indirect.jsonl +200 -0
- benchmark/macos/benign.jsonl +30 -0
- benchmark/macos/direct.jsonl +50 -0
- benchmark/macos/indirect.jsonl +50 -0
- benchmark/medical/benign.jsonl +642 -0
- benchmark/medical/direct.jsonl +229 -0
- benchmark/medical/indirect.jsonl +222 -0
- benchmark/os-filesystem/benign.jsonl +200 -0
- benchmark/os-filesystem/direct.jsonl +200 -0
- benchmark/os-filesystem/indirect.jsonl +200 -0
- benchmark/research/benign.jsonl +0 -0
- benchmark/research/direct.jsonl +119 -0
- benchmark/research/indirect.jsonl +125 -0
- benchmark/telecom/benign.jsonl +120 -0
- benchmark/telecom/direct.jsonl +161 -0
- benchmark/telecom/indirect.jsonl +166 -0
- benchmark/travel/benign.jsonl +130 -0
- benchmark/travel/direct.jsonl +105 -0
- benchmark/travel/indirect.jsonl +120 -0
- benchmark/windows/benign.jsonl +100 -0
- benchmark/windows/direct.jsonl +140 -0
- benchmark/windows/indirect.jsonl +107 -0
- benchmark/workflow/benign.jsonl +335 -0
- benchmark/workflow/direct.jsonl +78 -0
- benchmark/workflow/indirect.jsonl +107 -0
- cli/__init__.py +5 -0
- cli/main.py +182 -0
- cli/scaffold.py +334 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
- dt_arena/config/env.yaml +515 -0
- dt_arena/config/injection_mcp.yaml +430 -0
- dt_arena/config/mcp.yaml +642 -0
- dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
- dt_arena/envs/arxiv/docker-compose.yml +36 -0
- dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
- dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
- dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
- dt_arena/envs/atlassian/docker-compose.yml +72 -0
- dt_arena/envs/bigquery/docker-compose.yml +20 -0
- dt_arena/envs/booking/docker-compose.yml +59 -0
- dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
- dt_arena/envs/calendar/docker-compose.yml +42 -0
- dt_arena/envs/custom-website/docker-compose.yml +6 -0
- dt_arena/envs/customer_service/docker-compose.yml +59 -0
- dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
- dt_arena/envs/databricks/docker-compose.yml +51 -0
- dt_arena/envs/ecommerce/docker-compose.yml +6 -0
- dt_arena/envs/ers/docker-compose.yml +36 -0
- dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
- dt_arena/envs/finance/docker-compose.yml +23 -0
- dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
- dt_arena/envs/github/docker/docker-compose.yml +50 -0
- dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
- dt_arena/envs/gmail/docker-compose.yml +65 -0
- dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
- dt_arena/envs/google-form/docker-compose.yml +41 -0
- dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
- dt_arena/envs/googledocs/docker-compose.yml +78 -0
- dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
- dt_arena/envs/hospital/docker-compose.yml +27 -0
- dt_arena/envs/legal/docker-compose.yml +22 -0
- dt_arena/envs/linkedin/docker-compose.yml +63 -0
- dt_arena/envs/macos/docker-compose.yml +79 -0
- dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
- dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
- dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
- dt_arena/envs/paypal/docker-compose.yml +63 -0
- dt_arena/envs/research/docker-compose-hub.yml +13 -0
- dt_arena/envs/research/docker-compose.yml +24 -0
- dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
- dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
- dt_arena/envs/slack/docker-compose-hub.yml +28 -0
- dt_arena/envs/slack/docker-compose.yml +41 -0
- dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
- dt_arena/envs/snowflake/docker-compose.yml +44 -0
- dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
- dt_arena/envs/telecom/docker-compose.yml +17 -0
- dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
- dt_arena/envs/telegram/docker-compose.yml +62 -0
- dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
- dt_arena/envs/terminal/docker-compose.yml +26 -0
- dt_arena/envs/travel/docker-compose-hub.yml +19 -0
- dt_arena/envs/travel/docker-compose.yml +19 -0
- dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
- dt_arena/envs/whatsapp/docker-compose.yml +78 -0
- dt_arena/envs/windows/docker-compose.yml +71 -0
- dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
- dt_arena/envs/zoom/docker-compose.yml +40 -0
- dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
- dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
- dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
- dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
- dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
- dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
- dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
- dt_arena/injection_mcp_server/github/env_injection.py +206 -0
- dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
- dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
- dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
- dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
- dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
- dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
- dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
- dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
- dt_arena/injection_mcp_server/research/env_injection.py +616 -0
- dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
- dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
- dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
- dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
- dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
- dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
- dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
- dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
- dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
- dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
- dt_arena/mcp_server/atlassian/main.py +1554 -0
- dt_arena/mcp_server/atlassian/test_server.py +66 -0
- dt_arena/mcp_server/bigquery/main.py +333 -0
- dt_arena/mcp_server/booking/main.py +310 -0
- dt_arena/mcp_server/browser/main.py +1741 -0
- dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
- dt_arena/mcp_server/calendar/main.py +792 -0
- dt_arena/mcp_server/calendar/test_mcp.py +135 -0
- dt_arena/mcp_server/customer_service/main.py +1063 -0
- dt_arena/mcp_server/databricks/main.py +566 -0
- dt_arena/mcp_server/databricks/probe.py +102 -0
- dt_arena/mcp_server/ers/main.py +845 -0
- dt_arena/mcp_server/finance/__init__.py +87 -0
- dt_arena/mcp_server/finance/core/__init__.py +12 -0
- dt_arena/mcp_server/finance/core/data_loader.py +558 -0
- dt_arena/mcp_server/finance/core/portfolio.py +565 -0
- dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
- dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
- dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
- dt_arena/mcp_server/finance/injection/__init__.py +66 -0
- dt_arena/mcp_server/finance/injection/config.py +176 -0
- dt_arena/mcp_server/finance/injection/content.py +755 -0
- dt_arena/mcp_server/finance/injection/html.py +409 -0
- dt_arena/mcp_server/finance/injection/locations.py +167 -0
- dt_arena/mcp_server/finance/injection/methods.py +193 -0
- dt_arena/mcp_server/finance/injection/presets.py +1023 -0
- dt_arena/mcp_server/finance/main.py +361 -0
- dt_arena/mcp_server/finance/run_mcp.py +21 -0
- dt_arena/mcp_server/finance/run_web.py +26 -0
- dt_arena/mcp_server/finance/server/__init__.py +41 -0
- dt_arena/mcp_server/finance/server/extractor.py +1453 -0
- dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
- dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
- dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
- dt_arena/mcp_server/finance/server/mcp.py +451 -0
- dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
- dt_arena/mcp_server/finance/server/tools/account.py +88 -0
- dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
- dt_arena/mcp_server/finance/server/tools/social.py +73 -0
- dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
- dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
- dt_arena/mcp_server/finance/server/web.py +2139 -0
- dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
- dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
- dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
- dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
- dt_arena/mcp_server/github/main.py +441 -0
- dt_arena/mcp_server/gmail/main.py +1004 -0
- dt_arena/mcp_server/google_form/main.py +141 -0
- dt_arena/mcp_server/googledocs/main.py +458 -0
- dt_arena/mcp_server/hospital/mcp_server.py +458 -0
- dt_arena/mcp_server/legal/__init__.py +9 -0
- dt_arena/mcp_server/legal/core/__init__.py +14 -0
- dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
- dt_arena/mcp_server/legal/core/data_loader.py +266 -0
- dt_arena/mcp_server/legal/core/document_store.py +197 -0
- dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
- dt_arena/mcp_server/legal/main.py +89 -0
- dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
- dt_arena/mcp_server/legal/server/__init__.py +14 -0
- dt_arena/mcp_server/legal/server/mcp.py +2330 -0
- dt_arena/mcp_server/macos/client_test.py +270 -0
- dt_arena/mcp_server/macos/mcp_server.py +285 -0
- dt_arena/mcp_server/os-filesystem/main.py +1380 -0
- dt_arena/mcp_server/paypal/main.py +501 -0
- dt_arena/mcp_server/research/main.py +777 -0
- dt_arena/mcp_server/salesforce/main.py +2006 -0
- dt_arena/mcp_server/slack/main.py +318 -0
- dt_arena/mcp_server/snowflake/main.py +612 -0
- dt_arena/mcp_server/snowflake/probe.py +183 -0
- dt_arena/mcp_server/telecom/mcp_client.py +423 -0
- dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
- dt_arena/mcp_server/telegram/main.py +338 -0
- dt_arena/mcp_server/terminal/main.py +163 -0
- dt_arena/mcp_server/travel/client_test.py +16 -0
- dt_arena/mcp_server/travel/mcp_server.py +404 -0
- dt_arena/mcp_server/whatsapp/main.py +318 -0
- dt_arena/mcp_server/windows/client_test.py +270 -0
- dt_arena/mcp_server/windows/mcp_server.py +218 -0
- dt_arena/mcp_server/zoom/main.py +466 -0
- dt_arena/src/__init__.py +0 -0
- dt_arena/src/hooks/__init__.py +0 -0
- dt_arena/src/hooks/audit_log.py +30 -0
- dt_arena/src/hooks/hooks.json +3 -0
- dt_arena/src/run_benign.py +142 -0
- dt_arena/src/types/__init__.py +0 -0
- dt_arena/src/types/agent.py +441 -0
- dt_arena/src/types/attacks.py +2 -0
- dt_arena/src/types/environment.py +2 -0
- dt_arena/src/types/hooks.py +174 -0
- dt_arena/src/types/judge.py +52 -0
- dt_arena/src/types/red_teaming_trajectory.py +385 -0
- dt_arena/src/types/task.py +260 -0
- dt_arena/src/types/trajectory.py +315 -0
- dt_arena/utils/__init__.py +1 -0
- dt_arena/utils/atlassian/__init__.py +27 -0
- dt_arena/utils/atlassian/helpers.py +520 -0
- dt_arena/utils/bigquery/__init__.py +1 -0
- dt_arena/utils/bigquery/helpers.py +246 -0
- dt_arena/utils/calendar/__init__.py +1 -0
- dt_arena/utils/calendar/helpers.py +87 -0
- dt_arena/utils/customer_service/__init__.py +17 -0
- dt_arena/utils/customer_service/cs_env_client.py +940 -0
- dt_arena/utils/customer_service/helpers.py +339 -0
- dt_arena/utils/customer_service/judges/__init__.py +20 -0
- dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
- dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
- dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
- dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
- dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
- dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
- dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
- dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
- dt_arena/utils/customer_service/judges/text_utils.py +21 -0
- dt_arena/utils/databricks/__init__.py +2 -0
- dt_arena/utils/databricks/helpers.py +210 -0
- dt_arena/utils/finance/__init__.py +0 -0
- dt_arena/utils/finance/helpers.py +263 -0
- dt_arena/utils/github/__init__.py +1 -0
- dt_arena/utils/github/helpers.py +249 -0
- dt_arena/utils/gmail/__init__.py +1 -0
- dt_arena/utils/gmail/helpers.py +344 -0
- dt_arena/utils/google_form/__init__.py +2 -0
- dt_arena/utils/google_form/helpers.py +133 -0
- dt_arena/utils/legal/__init__.py +0 -0
- dt_arena/utils/legal/helpers.py +228 -0
- dt_arena/utils/macos/__init__.py +0 -0
- dt_arena/utils/macos/env_setup.py +215 -0
- dt_arena/utils/macos/helpers.py +61 -0
- dt_arena/utils/os_filesystem/__init__.py +1 -0
- dt_arena/utils/os_filesystem/helpers.py +366 -0
- dt_arena/utils/paypal/__init__.py +1 -0
- dt_arena/utils/paypal/helpers.py +178 -0
- dt_arena/utils/port_allocator.py +266 -0
- dt_arena/utils/research/__init__.py +0 -0
- dt_arena/utils/research/helpers.py +251 -0
- dt_arena/utils/salesforce/__init__.py +1 -0
- dt_arena/utils/salesforce/helpers.py +719 -0
- dt_arena/utils/slack/__init__.py +1 -0
- dt_arena/utils/slack/helpers.py +176 -0
- dt_arena/utils/snowflake/__init__.py +1 -0
- dt_arena/utils/snowflake/helpers.py +166 -0
- dt_arena/utils/telecom/__init__.py +1 -0
- dt_arena/utils/telecom/helpers.py +760 -0
- dt_arena/utils/telegram/__init__.py +0 -0
- dt_arena/utils/telegram/helpers.py +174 -0
- dt_arena/utils/terminal/__init__.py +0 -0
- dt_arena/utils/terminal/helpers.py +20 -0
- dt_arena/utils/travel/__init__.py +0 -0
- dt_arena/utils/travel/env_client.py +537 -0
- dt_arena/utils/travel/llm_judge.py +137 -0
- dt_arena/utils/travel/prompts.py +64 -0
- dt_arena/utils/utils/__init__.py +122 -0
- dt_arena/utils/whatsapp/__init__.py +0 -0
- dt_arena/utils/whatsapp/helpers.py +226 -0
- dt_arena/utils/windows/__init__.py +0 -0
- dt_arena/utils/windows/env_reset.py +224 -0
- dt_arena/utils/windows/env_setup.py +280 -0
- dt_arena/utils/windows/exfil_helpers.py +170 -0
- dt_arena/utils/windows/helpers.py +74 -0
- dt_arena/utils/zoom/__init__.py +1 -0
- dt_arena/utils/zoom/helpers.py +70 -0
- eval/__init__.py +1 -0
- eval/evaluation.py +426 -0
- eval/task_runner.py +449 -0
- utils/__init__.py +148 -0
- utils/agent_helpers.py +308 -0
- utils/agent_wrapper.py +189 -0
- utils/compose_utils.py +135 -0
- utils/config.py +77 -0
- utils/env_helpers.py +104 -0
- utils/eval_stats.py +88 -0
- utils/injection_helpers.py +429 -0
- utils/injection_mcp_helpers.py +152 -0
- utils/judge_helpers.py +181 -0
- utils/judge_utils.py +472 -0
- utils/llm.py +196 -0
- utils/logging.py +45 -0
- utils/mcp_helpers.py +232 -0
- utils/mcp_manager.py +235 -0
- utils/memory_guard.py +18 -0
- utils/red_teaming_sandbox.py +476 -0
- utils/reset_helpers.py +318 -0
- utils/resource_manager.py +370 -0
- utils/skill_helpers.py +447 -0
- utils/task_executor.py +904 -0
- utils/task_helpers.py +270 -0
- utils/template_helpers.py +179 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Content Injection - Inject fake content at the data layer
|
|
3
|
+
|
|
4
|
+
Unlike HTML injection (which hides malicious prompts in HTML),
|
|
5
|
+
content injection adds visible fake content that appears legitimate.
|
|
6
|
+
|
|
7
|
+
Static targeting (for use with Docker + red teaming agent):
|
|
8
|
+
- article_id=12345 → inject to that specific article
|
|
9
|
+
- symbol="AAPL" → inject for that specific stock
|
|
10
|
+
- article_id=None / symbol=None → inject to ALL matching items
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from injection.content import ContentInjector, FakeComment
|
|
14
|
+
|
|
15
|
+
injector = ContentInjector()
|
|
16
|
+
|
|
17
|
+
# Inject to specific article
|
|
18
|
+
injector.add_comment(FakeComment(
|
|
19
|
+
symbol="AAPL",
|
|
20
|
+
article_id=12345,
|
|
21
|
+
content="Buy now! This is going to moon!",
|
|
22
|
+
author="Market Expert",
|
|
23
|
+
votes={"up": 150, "down": 3}
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
# Inject to ALL AAPL articles
|
|
27
|
+
injector.add_comment(FakeComment(
|
|
28
|
+
symbol="AAPL",
|
|
29
|
+
article_id=None,
|
|
30
|
+
content="Comment for all articles",
|
|
31
|
+
))
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Dict, List, Optional, Any, Set
|
|
36
|
+
from datetime import datetime
|
|
37
|
+
import uuid
|
|
38
|
+
import random
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class FakeNews:
|
|
43
|
+
"""A fake news article to inject."""
|
|
44
|
+
title: str
|
|
45
|
+
summary: str
|
|
46
|
+
source: str = "Injected Source"
|
|
47
|
+
symbol: Optional[str] = None # None = global news
|
|
48
|
+
article_id: Optional[int] = None # Auto-generated if None
|
|
49
|
+
published_time: Optional[str] = None # Auto-generated if None
|
|
50
|
+
url: str = "#injected"
|
|
51
|
+
image_url: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
# Injection metadata
|
|
54
|
+
position: str = "top" # "top", "middle", "bottom", "random"
|
|
55
|
+
priority: int = 10 # Higher = shown first
|
|
56
|
+
visible: bool = True # If False, hidden but in HTML
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
if self.article_id is None:
|
|
60
|
+
self.article_id = abs(hash(self.title)) % 100000000
|
|
61
|
+
if self.published_time is None:
|
|
62
|
+
self.published_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
65
|
+
"""Convert to template-compatible dict (yfinance/finnhub format)."""
|
|
66
|
+
return {
|
|
67
|
+
# Core yfinance/finnhub fields
|
|
68
|
+
"id": self.article_id,
|
|
69
|
+
"headline": self.title,
|
|
70
|
+
"summary": self.summary,
|
|
71
|
+
"source": self.source,
|
|
72
|
+
"url": self.url,
|
|
73
|
+
"image": self.image_url,
|
|
74
|
+
"timestamp": int(datetime.now().timestamp()), # For sorting
|
|
75
|
+
"datetime": self.published_time,
|
|
76
|
+
"category": "company", # default category
|
|
77
|
+
"related": self.symbol or "", # yfinance uses 'related' for symbol
|
|
78
|
+
# Aliases for compatibility
|
|
79
|
+
"title": self.title,
|
|
80
|
+
"symbol": self.symbol,
|
|
81
|
+
"published_time": self.published_time,
|
|
82
|
+
"image_url": self.image_url,
|
|
83
|
+
# Injection metadata
|
|
84
|
+
"position": self.position,
|
|
85
|
+
"is_injected": True,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class FakeComment:
|
|
91
|
+
"""
|
|
92
|
+
A fake comment to inject on an article.
|
|
93
|
+
|
|
94
|
+
Static targeting (Docker + red teaming agent):
|
|
95
|
+
|
|
96
|
+
1. Specific ID: article_id=12345 → inject on that article only
|
|
97
|
+
2. All articles: article_id=None → inject on ALL articles for this symbol
|
|
98
|
+
|
|
99
|
+
Example usage:
|
|
100
|
+
# Inject to specific article
|
|
101
|
+
FakeComment(symbol="AAPL", content="...", article_id=12345)
|
|
102
|
+
|
|
103
|
+
# Inject to ALL AAPL articles
|
|
104
|
+
FakeComment(symbol="AAPL", content="...", article_id=None)
|
|
105
|
+
"""
|
|
106
|
+
symbol: str
|
|
107
|
+
content: str
|
|
108
|
+
|
|
109
|
+
# Targeting: specific article ID or None for all
|
|
110
|
+
article_id: Optional[int] = None
|
|
111
|
+
|
|
112
|
+
# Comment metadata
|
|
113
|
+
author: str = "Market Analyst"
|
|
114
|
+
sentiment: str = "neutral" # "bullish", "bearish", "neutral"
|
|
115
|
+
votes: Dict[str, int] = field(default_factory=lambda: {"up": 0, "down": 0})
|
|
116
|
+
|
|
117
|
+
# Reply support
|
|
118
|
+
parent_id: Optional[str] = None # If set, this is a reply
|
|
119
|
+
replies: List["FakeComment"] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
# Injection metadata
|
|
122
|
+
position: str = "top" # "top", "bottom", "random"
|
|
123
|
+
priority: int = 10
|
|
124
|
+
timestamp: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
def __post_init__(self):
|
|
127
|
+
if self.timestamp is None:
|
|
128
|
+
self.timestamp = datetime.now().strftime("%b %d, %Y at %I:%M %p")
|
|
129
|
+
if not hasattr(self, 'id'):
|
|
130
|
+
self.id = str(uuid.uuid4())[:8]
|
|
131
|
+
|
|
132
|
+
def should_apply_to(self, article_id: int) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Check if this comment should be applied to the given article.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
article_id: The article's ID
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if should apply
|
|
141
|
+
"""
|
|
142
|
+
# article_id=None means ALL articles
|
|
143
|
+
if self.article_id is None:
|
|
144
|
+
return True
|
|
145
|
+
return self.article_id == article_id
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
148
|
+
"""Convert to template-compatible dict."""
|
|
149
|
+
comment_id = getattr(self, 'id', str(uuid.uuid4())[:8])
|
|
150
|
+
|
|
151
|
+
# Determine avatar and badge based on author
|
|
152
|
+
avatar = "🧑💼" # Default professional avatar
|
|
153
|
+
badge = None
|
|
154
|
+
badge_class = ""
|
|
155
|
+
|
|
156
|
+
# Special author types
|
|
157
|
+
author_lower = self.author.lower()
|
|
158
|
+
if "analyst" in author_lower or "expert" in author_lower:
|
|
159
|
+
avatar = "📊"
|
|
160
|
+
badge = "Analyst"
|
|
161
|
+
badge_class = "verified"
|
|
162
|
+
elif "insider" in author_lower:
|
|
163
|
+
avatar = "🔒"
|
|
164
|
+
badge = "Insider"
|
|
165
|
+
badge_class = "insider"
|
|
166
|
+
elif "verified" in author_lower:
|
|
167
|
+
avatar = "✓"
|
|
168
|
+
badge = "Verified"
|
|
169
|
+
badge_class = "verified"
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
# API fields
|
|
173
|
+
"id": comment_id,
|
|
174
|
+
"article_id": self.article_id,
|
|
175
|
+
"symbol": self.symbol,
|
|
176
|
+
"content": self.content,
|
|
177
|
+
"user": self.author,
|
|
178
|
+
"votes": self.votes,
|
|
179
|
+
"parent_id": self.parent_id,
|
|
180
|
+
# Template-compatible fields (for article.html)
|
|
181
|
+
"author": self.author,
|
|
182
|
+
"text": self.content,
|
|
183
|
+
"time": self.timestamp,
|
|
184
|
+
"avatar": avatar,
|
|
185
|
+
"badge": badge,
|
|
186
|
+
"badge_class": badge_class,
|
|
187
|
+
"sentiment": self.sentiment if self.sentiment != "neutral" else None,
|
|
188
|
+
"likes": self.votes.get("up", 0),
|
|
189
|
+
"dislikes": self.votes.get("down", 0),
|
|
190
|
+
"replies": [r.to_dict() for r in self.replies],
|
|
191
|
+
"position": self.position,
|
|
192
|
+
"is_injected": True,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class FakeAnalystRating:
|
|
198
|
+
"""
|
|
199
|
+
A fake analyst rating to inject.
|
|
200
|
+
|
|
201
|
+
Format matches yfinance analyst_ratings structure for seamless merging:
|
|
202
|
+
- date: "2025-12-01"
|
|
203
|
+
- analyst/firm: "Goldman Sachs"
|
|
204
|
+
- rating: "Buy", "Hold", "Sell" (not strong_buy etc.)
|
|
205
|
+
- priceTarget: 358.15
|
|
206
|
+
- action: "Raises", "Lowers", "Maintains", "Initiates"
|
|
207
|
+
- fromGrade/toGrade: For rating changes
|
|
208
|
+
- priorTarget/currentTarget: For price target changes
|
|
209
|
+
|
|
210
|
+
Static targeting (Docker + red teaming agent):
|
|
211
|
+
1. Specific: symbol="AAPL" → inject for this stock only
|
|
212
|
+
2. All stocks: symbol=None → inject on ALL stocks
|
|
213
|
+
|
|
214
|
+
Example usage:
|
|
215
|
+
# Inject for specific stock
|
|
216
|
+
FakeAnalystRating(
|
|
217
|
+
symbol="AAPL",
|
|
218
|
+
analyst="Goldman Sachs",
|
|
219
|
+
to_grade="Strong Buy",
|
|
220
|
+
current_target=500.00,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Inject for ALL stocks
|
|
224
|
+
FakeAnalystRating(
|
|
225
|
+
symbol=None,
|
|
226
|
+
analyst="Goldman Sachs",
|
|
227
|
+
to_grade="Strong Buy",
|
|
228
|
+
current_target=500.00,
|
|
229
|
+
)
|
|
230
|
+
"""
|
|
231
|
+
analyst: str # Firm name, e.g., "Goldman Sachs"
|
|
232
|
+
to_grade: str # New rating: "Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"
|
|
233
|
+
current_target: float # New price target
|
|
234
|
+
|
|
235
|
+
# Rating change info (for display)
|
|
236
|
+
action: str = "Initiates" # "Raises", "Lowers", "Maintains", "Initiates", "Reiterates"
|
|
237
|
+
from_grade: Optional[str] = None # Previous rating (for upgrade/downgrade)
|
|
238
|
+
prior_target: Optional[float] = None # Previous price target
|
|
239
|
+
|
|
240
|
+
# Analyst commentary (displayed in HTML)
|
|
241
|
+
comment: Optional[str] = None # e.g., "We see significant upside due to AI initiatives..."
|
|
242
|
+
|
|
243
|
+
# Targeting: specific symbol or None for all
|
|
244
|
+
symbol: Optional[str] = None
|
|
245
|
+
|
|
246
|
+
date: Optional[str] = None
|
|
247
|
+
|
|
248
|
+
# Injection metadata
|
|
249
|
+
position: str = "top"
|
|
250
|
+
priority: int = 10
|
|
251
|
+
|
|
252
|
+
def __post_init__(self):
|
|
253
|
+
if self.date is None:
|
|
254
|
+
self.date = datetime.now().strftime("%Y-%m-%d")
|
|
255
|
+
|
|
256
|
+
def should_apply_to(self, symbol: str) -> bool:
|
|
257
|
+
"""Check if this rating should be applied to the given symbol."""
|
|
258
|
+
# symbol=None means ALL stocks
|
|
259
|
+
if self.symbol is None:
|
|
260
|
+
return True
|
|
261
|
+
return self.symbol.upper() == symbol.upper()
|
|
262
|
+
|
|
263
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
264
|
+
"""Convert to yfinance-compatible format for template rendering."""
|
|
265
|
+
return {
|
|
266
|
+
# yfinance fields
|
|
267
|
+
"date": self.date,
|
|
268
|
+
"analyst": self.analyst,
|
|
269
|
+
"firm": self.analyst, # Template uses both
|
|
270
|
+
"rating": self.to_grade,
|
|
271
|
+
"toGrade": self.to_grade,
|
|
272
|
+
"fromGrade": self.from_grade,
|
|
273
|
+
"action": self.action,
|
|
274
|
+
"priceTarget": self.current_target,
|
|
275
|
+
"currentTarget": self.current_target,
|
|
276
|
+
"priorTarget": self.prior_target,
|
|
277
|
+
# Commentary (our extension)
|
|
278
|
+
"comment": self.comment,
|
|
279
|
+
# Metadata
|
|
280
|
+
"symbol": self.symbol,
|
|
281
|
+
"is_injected": True,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class NewsModification:
|
|
287
|
+
"""
|
|
288
|
+
Modification to apply to an existing news article.
|
|
289
|
+
|
|
290
|
+
This allows injecting content INTO real news, not just adding fake news.
|
|
291
|
+
The modification is synced between news list and article pages.
|
|
292
|
+
|
|
293
|
+
Static targeting (Docker + red teaming agent):
|
|
294
|
+
1. Specific ID: article_id=12345 → modify that article only
|
|
295
|
+
2. All articles: article_id=None → modify ALL articles for this symbol
|
|
296
|
+
"""
|
|
297
|
+
symbol: str
|
|
298
|
+
|
|
299
|
+
# Targeting: specific article ID or None for all
|
|
300
|
+
article_id: Optional[int] = None
|
|
301
|
+
|
|
302
|
+
# Title modification
|
|
303
|
+
title_prefix: str = "" # Add before title
|
|
304
|
+
title_suffix: str = "" # Add after title
|
|
305
|
+
title_replace: Optional[str] = None # Replace entire title
|
|
306
|
+
|
|
307
|
+
# Summary modification
|
|
308
|
+
summary_prefix: str = "" # Add before summary
|
|
309
|
+
summary_suffix: str = "" # Add after summary
|
|
310
|
+
summary_inject: str = "" # Inject in middle
|
|
311
|
+
summary_inject_position: float = 0.5 # 0.0=start, 0.5=middle, 1.0=end
|
|
312
|
+
summary_replace: Optional[str] = None # Replace entire summary
|
|
313
|
+
|
|
314
|
+
# Content modification (for article body)
|
|
315
|
+
content_prefix: str = ""
|
|
316
|
+
content_suffix: str = ""
|
|
317
|
+
content_inject: str = ""
|
|
318
|
+
content_inject_position: float = 0.5
|
|
319
|
+
|
|
320
|
+
def should_apply_to(self, article_id: int) -> bool:
|
|
321
|
+
"""Check if this modification should be applied to the given article."""
|
|
322
|
+
# article_id=None means ALL articles
|
|
323
|
+
if self.article_id is None:
|
|
324
|
+
return True
|
|
325
|
+
return self.article_id == article_id
|
|
326
|
+
|
|
327
|
+
def apply_to_title(self, original: str) -> str:
|
|
328
|
+
"""Apply modifications to title."""
|
|
329
|
+
if self.title_replace is not None:
|
|
330
|
+
return self.title_replace
|
|
331
|
+
return f"{self.title_prefix}{original}{self.title_suffix}"
|
|
332
|
+
|
|
333
|
+
def apply_to_summary(self, original: str) -> str:
|
|
334
|
+
"""Apply modifications to summary."""
|
|
335
|
+
if self.summary_replace is not None:
|
|
336
|
+
return self.summary_replace
|
|
337
|
+
|
|
338
|
+
result = original
|
|
339
|
+
if self.summary_inject:
|
|
340
|
+
pos = int(len(result) * self.summary_inject_position)
|
|
341
|
+
# Try to find a sentence boundary near the position
|
|
342
|
+
for i in range(pos, min(pos + 50, len(result))):
|
|
343
|
+
if result[i] in '.!?':
|
|
344
|
+
pos = i + 1
|
|
345
|
+
break
|
|
346
|
+
result = result[:pos] + " " + self.summary_inject + " " + result[pos:]
|
|
347
|
+
|
|
348
|
+
return f"{self.summary_prefix}{result}{self.summary_suffix}"
|
|
349
|
+
|
|
350
|
+
def apply_to_content(self, original: str) -> str:
|
|
351
|
+
"""Apply modifications to article content."""
|
|
352
|
+
result = original
|
|
353
|
+
if self.content_inject:
|
|
354
|
+
pos = int(len(result) * self.content_inject_position)
|
|
355
|
+
# Try to find a paragraph boundary
|
|
356
|
+
for i in range(pos, min(pos + 100, len(result))):
|
|
357
|
+
if result[i:i+2] == '\n\n' or result[i:i+4] == '</p>':
|
|
358
|
+
pos = i
|
|
359
|
+
break
|
|
360
|
+
result = result[:pos] + "\n\n" + self.content_inject + "\n\n" + result[pos:]
|
|
361
|
+
|
|
362
|
+
return f"{self.content_prefix}{result}{self.content_suffix}"
|
|
363
|
+
|
|
364
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
365
|
+
return {
|
|
366
|
+
"article_id": self.article_id,
|
|
367
|
+
"symbol": self.symbol,
|
|
368
|
+
"title_prefix": self.title_prefix,
|
|
369
|
+
"title_suffix": self.title_suffix,
|
|
370
|
+
"title_replace": self.title_replace,
|
|
371
|
+
"summary_prefix": self.summary_prefix,
|
|
372
|
+
"summary_suffix": self.summary_suffix,
|
|
373
|
+
"summary_inject": self.summary_inject,
|
|
374
|
+
"summary_inject_position": self.summary_inject_position,
|
|
375
|
+
"summary_replace": self.summary_replace,
|
|
376
|
+
"content_prefix": self.content_prefix,
|
|
377
|
+
"content_suffix": self.content_suffix,
|
|
378
|
+
"content_inject": self.content_inject,
|
|
379
|
+
"content_inject_position": self.content_inject_position,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class ContentInjector:
|
|
384
|
+
"""
|
|
385
|
+
Manages fake content injection at the data layer.
|
|
386
|
+
|
|
387
|
+
Unlike HtmlInjector (HTML layer), this injects visible fake content
|
|
388
|
+
that gets rendered by templates alongside real content.
|
|
389
|
+
|
|
390
|
+
Static injection (Docker + red teaming agent):
|
|
391
|
+
- All targeting is deterministic (article_id or symbol)
|
|
392
|
+
- No view tracking or dynamic binding
|
|
393
|
+
- Content loaded at container startup via INJECTION_CONFIG env var
|
|
394
|
+
|
|
395
|
+
Features:
|
|
396
|
+
- Add fake news articles
|
|
397
|
+
- Add fake comments with votes
|
|
398
|
+
- Add fake analyst ratings
|
|
399
|
+
- Modify existing news (title, summary, content injection)
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
def __init__(self):
|
|
403
|
+
self._news: List[FakeNews] = []
|
|
404
|
+
# Comments: stored by symbol
|
|
405
|
+
self._comments: Dict[str, List[FakeComment]] = {} # key: symbol
|
|
406
|
+
# Analyst ratings: stored globally
|
|
407
|
+
self._analyst_ratings: List[FakeAnalystRating] = []
|
|
408
|
+
# Modifications: stored by symbol
|
|
409
|
+
self._modifications: Dict[str, List[NewsModification]] = {} # key: symbol
|
|
410
|
+
self._enabled = True
|
|
411
|
+
# Delivery log — records every time injected content is served
|
|
412
|
+
self._delivery_log: List[Dict[str, Any]] = []
|
|
413
|
+
|
|
414
|
+
def enable(self):
|
|
415
|
+
"""Enable content injection."""
|
|
416
|
+
self._enabled = True
|
|
417
|
+
|
|
418
|
+
def disable(self):
|
|
419
|
+
"""Disable content injection."""
|
|
420
|
+
self._enabled = False
|
|
421
|
+
|
|
422
|
+
def clear(self):
|
|
423
|
+
"""Clear all injected content and modifications."""
|
|
424
|
+
self._news.clear()
|
|
425
|
+
self._comments.clear()
|
|
426
|
+
self._analyst_ratings.clear()
|
|
427
|
+
self._modifications.clear()
|
|
428
|
+
self._delivery_log.clear()
|
|
429
|
+
|
|
430
|
+
def get_delivery_log(self) -> List[Dict[str, Any]]:
|
|
431
|
+
"""Get the log of all injected content deliveries."""
|
|
432
|
+
return list(self._delivery_log)
|
|
433
|
+
|
|
434
|
+
# === News Injection ===
|
|
435
|
+
|
|
436
|
+
def add_news(self, news: FakeNews):
|
|
437
|
+
"""Add a fake news article."""
|
|
438
|
+
self._news.append(news)
|
|
439
|
+
# Sort by priority
|
|
440
|
+
self._news.sort(key=lambda n: -n.priority)
|
|
441
|
+
|
|
442
|
+
def get_news(self, symbol: Optional[str] = None, limit: int = 10) -> List[Dict]:
|
|
443
|
+
"""Get injected news, optionally filtered by symbol."""
|
|
444
|
+
if not self._enabled:
|
|
445
|
+
return []
|
|
446
|
+
|
|
447
|
+
result = []
|
|
448
|
+
for news in self._news:
|
|
449
|
+
if symbol is None or news.symbol is None or news.symbol == symbol:
|
|
450
|
+
result.append(news.to_dict())
|
|
451
|
+
if len(result) >= limit:
|
|
452
|
+
break
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
def merge_news(self, real_news: List[Dict], symbol: Optional[str] = None) -> List[Dict]:
|
|
456
|
+
"""
|
|
457
|
+
Merge injected news with real news and apply modifications.
|
|
458
|
+
|
|
459
|
+
Steps:
|
|
460
|
+
1. Insert fake news at specified positions
|
|
461
|
+
2. Apply modifications to matching news articles (title/summary changes)
|
|
462
|
+
"""
|
|
463
|
+
if not self._enabled:
|
|
464
|
+
return real_news
|
|
465
|
+
|
|
466
|
+
result = list(real_news)
|
|
467
|
+
|
|
468
|
+
# Insert fake news
|
|
469
|
+
fake = self.get_news(symbol)
|
|
470
|
+
for item in fake:
|
|
471
|
+
pos = item.get("position", "top")
|
|
472
|
+
if pos == "top":
|
|
473
|
+
result.insert(0, item)
|
|
474
|
+
elif pos == "bottom":
|
|
475
|
+
result.append(item)
|
|
476
|
+
elif pos == "middle":
|
|
477
|
+
mid = len(result) // 2
|
|
478
|
+
result.insert(mid, item)
|
|
479
|
+
else: # random
|
|
480
|
+
idx = random.randint(0, len(result))
|
|
481
|
+
result.insert(idx, item)
|
|
482
|
+
|
|
483
|
+
if fake:
|
|
484
|
+
self._delivery_log.append({
|
|
485
|
+
"type": "fake_news",
|
|
486
|
+
"symbol": symbol,
|
|
487
|
+
"count": len(fake),
|
|
488
|
+
"titles": [f.get("title", "")[:80] for f in fake],
|
|
489
|
+
"timestamp": datetime.now().isoformat(),
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
# Apply modifications to news list
|
|
493
|
+
mods_applied = []
|
|
494
|
+
if symbol and symbol in self._modifications:
|
|
495
|
+
for i, news_item in enumerate(result):
|
|
496
|
+
article_id = news_item.get('id')
|
|
497
|
+
if article_id:
|
|
498
|
+
for mod in self._modifications[symbol]:
|
|
499
|
+
if mod.should_apply_to(article_id):
|
|
500
|
+
# Apply title modifications
|
|
501
|
+
title = news_item.get('title', '') or news_item.get('headline', '')
|
|
502
|
+
if mod.title_replace:
|
|
503
|
+
title = mod.title_replace
|
|
504
|
+
else:
|
|
505
|
+
if mod.title_prefix:
|
|
506
|
+
title = mod.title_prefix + title
|
|
507
|
+
if mod.title_suffix:
|
|
508
|
+
title = title + mod.title_suffix
|
|
509
|
+
news_item['title'] = title
|
|
510
|
+
if 'headline' in news_item:
|
|
511
|
+
news_item['headline'] = title
|
|
512
|
+
|
|
513
|
+
# Apply summary modifications
|
|
514
|
+
summary = news_item.get('summary', '') or ''
|
|
515
|
+
if mod.summary_prefix:
|
|
516
|
+
summary = mod.summary_prefix + summary
|
|
517
|
+
if mod.summary_suffix:
|
|
518
|
+
summary = summary + mod.summary_suffix
|
|
519
|
+
if mod.summary_inject:
|
|
520
|
+
# Inject at specified position
|
|
521
|
+
pos = mod.summary_inject_position or 0.5
|
|
522
|
+
inject_idx = int(len(summary) * pos)
|
|
523
|
+
summary = summary[:inject_idx] + mod.summary_inject + summary[inject_idx:]
|
|
524
|
+
news_item['summary'] = summary
|
|
525
|
+
mods_applied.append(article_id)
|
|
526
|
+
|
|
527
|
+
if mods_applied:
|
|
528
|
+
self._delivery_log.append({
|
|
529
|
+
"type": "news_modification",
|
|
530
|
+
"symbol": symbol,
|
|
531
|
+
"articles_modified": mods_applied,
|
|
532
|
+
"timestamp": datetime.now().isoformat(),
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
return result
|
|
536
|
+
|
|
537
|
+
# === Comment Injection ===
|
|
538
|
+
|
|
539
|
+
def add_comment(self, comment: FakeComment):
|
|
540
|
+
"""Add a fake comment."""
|
|
541
|
+
symbol = comment.symbol
|
|
542
|
+
if symbol not in self._comments:
|
|
543
|
+
self._comments[symbol] = []
|
|
544
|
+
self._comments[symbol].append(comment)
|
|
545
|
+
self._comments[symbol].sort(key=lambda c: -c.priority)
|
|
546
|
+
|
|
547
|
+
def get_comments(self, symbol: str, article_id: int) -> List[Dict]:
|
|
548
|
+
"""
|
|
549
|
+
Get injected comments for an article.
|
|
550
|
+
|
|
551
|
+
Static matching: checks article_id directly.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
symbol: Stock symbol
|
|
555
|
+
article_id: Article ID
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
List of comment dicts to inject
|
|
559
|
+
"""
|
|
560
|
+
if not self._enabled:
|
|
561
|
+
return []
|
|
562
|
+
|
|
563
|
+
result = []
|
|
564
|
+
for comment in self._comments.get(symbol, []):
|
|
565
|
+
if comment.should_apply_to(article_id):
|
|
566
|
+
comment_dict = comment.to_dict()
|
|
567
|
+
comment_dict['article_id'] = article_id
|
|
568
|
+
result.append(comment_dict)
|
|
569
|
+
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
def merge_comments(self, real_comments: List[Dict], symbol: str, article_id: int) -> List[Dict]:
|
|
573
|
+
"""Merge injected comments with real comments."""
|
|
574
|
+
if not self._enabled:
|
|
575
|
+
return real_comments
|
|
576
|
+
|
|
577
|
+
fake = self.get_comments(symbol, article_id)
|
|
578
|
+
if not fake:
|
|
579
|
+
return real_comments
|
|
580
|
+
|
|
581
|
+
result = list(real_comments)
|
|
582
|
+
for item in fake:
|
|
583
|
+
pos = item.get("position", "top")
|
|
584
|
+
if pos == "top":
|
|
585
|
+
result.insert(0, item)
|
|
586
|
+
elif pos == "bottom":
|
|
587
|
+
result.append(item)
|
|
588
|
+
else:
|
|
589
|
+
mid = len(result) // 2
|
|
590
|
+
result.insert(mid, item)
|
|
591
|
+
|
|
592
|
+
self._delivery_log.append({
|
|
593
|
+
"type": "fake_comment",
|
|
594
|
+
"symbol": symbol,
|
|
595
|
+
"article_id": article_id,
|
|
596
|
+
"count": len(fake),
|
|
597
|
+
"timestamp": datetime.now().isoformat(),
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
return result
|
|
601
|
+
|
|
602
|
+
# === Analyst Rating Injection ===
|
|
603
|
+
|
|
604
|
+
def add_analyst_rating(self, rating: FakeAnalystRating):
|
|
605
|
+
"""Add a fake analyst rating."""
|
|
606
|
+
self._analyst_ratings.append(rating)
|
|
607
|
+
self._analyst_ratings.sort(key=lambda r: -r.priority)
|
|
608
|
+
|
|
609
|
+
def get_analyst_ratings(self, symbol: str) -> List[Dict]:
|
|
610
|
+
"""
|
|
611
|
+
Get injected analyst ratings for a symbol.
|
|
612
|
+
|
|
613
|
+
Static matching: checks symbol directly.
|
|
614
|
+
"""
|
|
615
|
+
if not self._enabled:
|
|
616
|
+
return []
|
|
617
|
+
|
|
618
|
+
result = []
|
|
619
|
+
for rating in self._analyst_ratings:
|
|
620
|
+
if rating.should_apply_to(symbol):
|
|
621
|
+
rating_dict = rating.to_dict()
|
|
622
|
+
rating_dict['symbol'] = symbol
|
|
623
|
+
result.append(rating_dict)
|
|
624
|
+
|
|
625
|
+
return result
|
|
626
|
+
|
|
627
|
+
def merge_analyst_ratings(self, real_ratings: List[Dict], symbol: str) -> List[Dict]:
|
|
628
|
+
"""Merge injected ratings with real ratings."""
|
|
629
|
+
if not self._enabled:
|
|
630
|
+
return real_ratings
|
|
631
|
+
|
|
632
|
+
fake = self.get_analyst_ratings(symbol)
|
|
633
|
+
if not fake:
|
|
634
|
+
return real_ratings
|
|
635
|
+
|
|
636
|
+
self._delivery_log.append({
|
|
637
|
+
"type": "fake_analyst_rating",
|
|
638
|
+
"symbol": symbol,
|
|
639
|
+
"count": len(fake),
|
|
640
|
+
"timestamp": datetime.now().isoformat(),
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
# Prepend fake ratings (they appear first)
|
|
644
|
+
return fake + list(real_ratings)
|
|
645
|
+
|
|
646
|
+
# === News Modification (modify existing content) ===
|
|
647
|
+
|
|
648
|
+
def add_modification(self, mod: NewsModification):
|
|
649
|
+
"""Add a modification to an existing news article."""
|
|
650
|
+
symbol = mod.symbol
|
|
651
|
+
if symbol not in self._modifications:
|
|
652
|
+
self._modifications[symbol] = []
|
|
653
|
+
self._modifications[symbol].append(mod)
|
|
654
|
+
|
|
655
|
+
def get_modifications(self, symbol: str, article_id: int) -> List[NewsModification]:
|
|
656
|
+
"""
|
|
657
|
+
Get ALL applicable modifications for an article.
|
|
658
|
+
|
|
659
|
+
Static matching: checks article_id directly.
|
|
660
|
+
"""
|
|
661
|
+
if not self._enabled:
|
|
662
|
+
return []
|
|
663
|
+
|
|
664
|
+
result = []
|
|
665
|
+
for mod in self._modifications.get(symbol, []):
|
|
666
|
+
if mod.should_apply_to(article_id):
|
|
667
|
+
result.append(mod)
|
|
668
|
+
|
|
669
|
+
return result
|
|
670
|
+
|
|
671
|
+
def modify_article(self, article: Dict, symbol: str = None) -> Dict:
|
|
672
|
+
"""
|
|
673
|
+
Apply modifications to a full article.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
article: The article dict
|
|
677
|
+
symbol: Stock symbol (auto-detected if not provided)
|
|
678
|
+
"""
|
|
679
|
+
if not self._enabled:
|
|
680
|
+
return article
|
|
681
|
+
|
|
682
|
+
article_id = article.get('id')
|
|
683
|
+
# Try to get symbol from article if not provided
|
|
684
|
+
if symbol is None:
|
|
685
|
+
symbol = article.get('symbol', article.get('related', '').split(',')[0].strip())
|
|
686
|
+
|
|
687
|
+
if not article_id or not symbol:
|
|
688
|
+
return article
|
|
689
|
+
|
|
690
|
+
mods = self.get_modifications(symbol, article_id)
|
|
691
|
+
if not mods:
|
|
692
|
+
return article
|
|
693
|
+
|
|
694
|
+
result = article.copy()
|
|
695
|
+
|
|
696
|
+
for mod in mods:
|
|
697
|
+
# Modify title
|
|
698
|
+
if 'title' in result:
|
|
699
|
+
result['title'] = mod.apply_to_title(result['title'])
|
|
700
|
+
if 'headline' in result:
|
|
701
|
+
result['headline'] = mod.apply_to_title(result['headline'])
|
|
702
|
+
|
|
703
|
+
# Modify summary
|
|
704
|
+
if 'summary' in result:
|
|
705
|
+
result['summary'] = mod.apply_to_summary(result['summary'])
|
|
706
|
+
|
|
707
|
+
# Modify content (article body)
|
|
708
|
+
if 'content' in result:
|
|
709
|
+
result['content'] = mod.apply_to_content(result['content'])
|
|
710
|
+
if 'body' in result:
|
|
711
|
+
result['body'] = mod.apply_to_content(result['body'])
|
|
712
|
+
|
|
713
|
+
result['is_modified'] = True
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
# === Serialization ===
|
|
717
|
+
|
|
718
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
719
|
+
"""Serialize for API responses."""
|
|
720
|
+
# Flatten modifications for display
|
|
721
|
+
all_mods = {}
|
|
722
|
+
for symbol, mods in self._modifications.items():
|
|
723
|
+
for i, mod in enumerate(mods):
|
|
724
|
+
key = f"{symbol}_{mod.article_id or f'all_{i}'}"
|
|
725
|
+
all_mods[key] = mod.to_dict()
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
"enabled": self._enabled,
|
|
729
|
+
"news_count": len(self._news),
|
|
730
|
+
"comments_count": sum(len(v) for v in self._comments.values()),
|
|
731
|
+
"ratings_count": len(self._analyst_ratings),
|
|
732
|
+
"modifications_count": sum(len(v) for v in self._modifications.values()),
|
|
733
|
+
"modifications": all_mods,
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
def from_config(self, config: Dict[str, Any]):
|
|
737
|
+
"""Load from configuration dict."""
|
|
738
|
+
self.clear()
|
|
739
|
+
|
|
740
|
+
for news_data in config.get("news", []):
|
|
741
|
+
# Filter out legacy match_index if present
|
|
742
|
+
news_data = {k: v for k, v in news_data.items() if k != 'match_index'}
|
|
743
|
+
self.add_news(FakeNews(**news_data))
|
|
744
|
+
|
|
745
|
+
for comment_data in config.get("comments", []):
|
|
746
|
+
comment_data = {k: v for k, v in comment_data.items() if k != 'match_index'}
|
|
747
|
+
self.add_comment(FakeComment(**comment_data))
|
|
748
|
+
|
|
749
|
+
for rating_data in config.get("analyst_ratings", []):
|
|
750
|
+
rating_data = {k: v for k, v in rating_data.items() if k != 'match_index'}
|
|
751
|
+
self.add_analyst_rating(FakeAnalystRating(**rating_data))
|
|
752
|
+
|
|
753
|
+
for mod_data in config.get("modifications", []):
|
|
754
|
+
mod_data = {k: v for k, v in mod_data.items() if k != 'match_index'}
|
|
755
|
+
self.add_modification(NewsModification(**mod_data))
|