ff-taskforce 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.
Files changed (38) hide show
  1. ff_taskforce-0.1.0.dist-info/METADATA +136 -0
  2. ff_taskforce-0.1.0.dist-info/RECORD +38 -0
  3. ff_taskforce-0.1.0.dist-info/WHEEL +4 -0
  4. ff_taskforce-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. taskforce/__init__.py +3 -0
  6. taskforce/for-agent-layerinfo.md +16 -0
  7. taskforce/l1/__init__.py +3 -0
  8. taskforce/l1/for-agent-layerinfo-l1.md +8 -0
  9. taskforce/l1/schema/__init__.py +3 -0
  10. taskforce/l1/schema/for-agent-moduleinfo.md +47 -0
  11. taskforce/l1/schema/schema.py +35 -0
  12. taskforce/l2/__init__.py +6 -0
  13. taskforce/l2/config/__init__.py +3 -0
  14. taskforce/l2/config/config.py +65 -0
  15. taskforce/l2/config/for-agent-moduleinfo.md +24 -0
  16. taskforce/l2/cost_logger/__init__.py +3 -0
  17. taskforce/l2/cost_logger/cost_logger.py +48 -0
  18. taskforce/l2/cost_logger/for-agent-moduleinfo.md +21 -0
  19. taskforce/l2/for-agent-layerinfo-l2.md +17 -0
  20. taskforce/l2/panelist/__init__.py +3 -0
  21. taskforce/l2/panelist/for-agent-moduleinfo.md +23 -0
  22. taskforce/l2/panelist/panelist.py +52 -0
  23. taskforce/l2/session_logger/__init__.py +3 -0
  24. taskforce/l2/session_logger/for-agent-moduleinfo.md +22 -0
  25. taskforce/l2/session_logger/session_logger.py +48 -0
  26. taskforce/l3/__init__.py +3 -0
  27. taskforce/l3/for-agent-layerinfo-l3.md +7 -0
  28. taskforce/l3/roundtable/__init__.py +3 -0
  29. taskforce/l3/roundtable/for-agent-moduleinfo.md +30 -0
  30. taskforce/l3/roundtable/roundtable.py +102 -0
  31. taskforce/l4/__init__.py +3 -0
  32. taskforce/l4/for-agent-layerinfo-l4.md +6 -0
  33. taskforce/l4/taskforce_facade/__init__.py +3 -0
  34. taskforce/l4/taskforce_facade/for-agent-moduleinfo.md +25 -0
  35. taskforce/l4/taskforce_facade/taskforce_facade.py +40 -0
  36. taskforce/mcp_wrapper/__init__.py +3 -0
  37. taskforce/mcp_wrapper/__main__.py +3 -0
  38. taskforce/mcp_wrapper/mcp_wrapper.py +48 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: ff-taskforce
3
+ Version: 0.1.0
4
+ Summary: Multi-LLM roundtable for diverse perspective gathering
5
+ Project-URL: Repository, https://github.com/gatesplan/taskforce
6
+ Project-URL: Issues, https://github.com/gatesplan/taskforce/issues
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: agent,llm,mcp,multi-model,roundtable
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: litellm
19
+ Requires-Dist: loguru
20
+ Requires-Dist: pydantic>=2.0
21
+ Requires-Dist: python-dotenv
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp; extra == 'mcp'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # taskforce
27
+
28
+ AI agent-oriented multi-LLM roundtable library.
29
+
30
+ Multiple top-tier LLMs (GPT, Grok, Claude, Gemini) are queried in parallel with the same agenda, and the collected opinions are classified into **common / divergent / unique** perspectives, returned as a structured `IdeaPool`.
31
+
32
+ **This package is designed for AI agents, not for direct human use.**
33
+ The primary interface is the MCP wrapper (`roundtable_discuss` tool), which allows agents to invoke a roundtable discussion as a tool call. A Python API is also available for programmatic integration.
34
+
35
+ ## Quick Start
36
+
37
+ ### 1. Install
38
+
39
+ ```bash
40
+ pip install taskforce
41
+ ```
42
+
43
+ For MCP server support:
44
+
45
+ ```bash
46
+ pip install taskforce[mcp]
47
+ ```
48
+
49
+ ### 2. Set environment variables
50
+
51
+ At least two provider API keys are required (one will be excluded as the caller).
52
+ `XAI_API_KEY` is always required (used by the summarizer).
53
+
54
+ ```
55
+ OPENAI_API_KEY=sk-...
56
+ XAI_API_KEY=xai-...
57
+ ANTHROPIC_API_KEY=sk-ant-...
58
+ GEMINI_API_KEY=AI...
59
+ ```
60
+
61
+ ### 3. Use as MCP tool (recommended for agents)
62
+
63
+ Add to your MCP server config:
64
+
65
+ `TASKFORCE_CALLER_PROVIDER` is the provider of the agent that will call this tool.
66
+ The matching provider's model is excluded from the panel -- querying the same model that is already reasoning adds no diversity.
67
+ For example, if Claude Code is the caller, set it to `"anthropic"` so Claude is excluded from the panel.
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "taskforce": {
73
+ "command": "python",
74
+ "args": ["-m", "taskforce.mcp_wrapper"],
75
+ "env": {
76
+ "TASKFORCE_CALLER_PROVIDER": "anthropic"
77
+ }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ The agent can then call the `roundtable_discuss` tool with `agenda` and `context` parameters.
84
+
85
+ ### 4. Use as Python library
86
+
87
+ ```python
88
+ from taskforce import Taskforce
89
+
90
+ tf = Taskforce(caller_provider="anthropic")
91
+ pool = tf.discuss(
92
+ agenda="Evaluate the trade-offs of approach A vs B",
93
+ context="<detailed context here>"
94
+ )
95
+
96
+ # pool.common -- list[str]: points most models agree on
97
+ # pool.divergent -- list[DivergentPoint]: topics with differing positions
98
+ # pool.unique -- list[UniquePoint]: points raised by only one model
99
+ ```
100
+
101
+ ## Important Notes
102
+
103
+ - **Paid API calls.** Every `discuss()` invocation calls multiple LLM APIs in parallel. Agents should confirm with the user before calling.
104
+ - **caller_provider exclusion.** The model from the same provider as the calling agent is excluded from the panel to maximize perspective diversity.
105
+ - **XAI_API_KEY is mandatory.** The summarizer (grok-4-1-fast-non-reasoning) always uses the XAI key.
106
+ - **Rich context matters.** Input tokens are cheap. Provide as much context as possible -- specifications, constraints, background, decisions already made -- so the panel can give concrete, actionable opinions instead of generic advice.
107
+
108
+ ## API
109
+
110
+ ### `Taskforce(caller_provider, dotenv_path=None)`
111
+
112
+ - `caller_provider` (`str`): The LLM provider of the calling agent (e.g. `"anthropic"`, `"openai"`). That provider's model is excluded from the panel.
113
+ - `dotenv_path` (`str | None`): Path to `.env` file. Defaults to auto-discovery.
114
+
115
+ ### `Taskforce.discuss(agenda, context="") -> IdeaPool`
116
+
117
+ Synchronous wrapper. Queries the panel, summarizes, and returns an `IdeaPool`.
118
+
119
+ ### `Taskforce.discuss_async(agenda, context="") -> IdeaPool`
120
+
121
+ Async version for use in async contexts.
122
+
123
+ ### `IdeaPool`
124
+
125
+ | Field | Type | Description |
126
+ |-------|------|-------------|
127
+ | `agenda` | `str` | The original agenda |
128
+ | `common` | `list[str]` | Points most models agree on |
129
+ | `divergent` | `list[DivergentPoint]` | Topics with differing positions (`topic`, `positions: dict[model, position]`) |
130
+ | `unique` | `list[UniquePoint]` | Points from a single model (`point`, `source_model`) |
131
+ | `total_cost` | `float` | Total API cost (USD) |
132
+ | `total_tokens` | `int` | Total tokens consumed |
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,38 @@
1
+ taskforce/__init__.py,sha256=RbWmYRJP0g3wcDAMLkT7eonFt2sJAOSPbVM6KQqf96g,68
2
+ taskforce/for-agent-layerinfo.md,sha256=UiwhcCYsbgFRDW0SnzjsqxFQqrsI9T9mvkHveibruqk,472
3
+ taskforce/l1/__init__.py,sha256=WPhPxdLAWkaxuHfzNpxI3vKIDlFa9Q6w5cALlfHlFJM,161
4
+ taskforce/l1/for-agent-layerinfo-l1.md,sha256=rUXo5NFVScE3gAKChddkgf8pjNg0FWHg32N2k7lFw3E,327
5
+ taskforce/l1/schema/__init__.py,sha256=WPhPxdLAWkaxuHfzNpxI3vKIDlFa9Q6w5cALlfHlFJM,161
6
+ taskforce/l1/schema/for-agent-moduleinfo.md,sha256=17kQf4-KSmd6FIei2PbtbKiqlBlbR4FZvDFopb51PmY,1122
7
+ taskforce/l1/schema/schema.py,sha256=BRnavvnZp3NA9-vXqxpTAFW1utBoJsd2HNKlwtkUP1g,608
8
+ taskforce/l2/__init__.py,sha256=-m0agG1snzxPrr8Rud9hS7m4msCjp2KrO_FmHBkUIUo,207
9
+ taskforce/l2/for-agent-layerinfo-l2.md,sha256=Y5QgI7C7Coz3N5t_tKurfJy4Pgy8mXreRQuTMrRyf-0,622
10
+ taskforce/l2/config/__init__.py,sha256=IMyl3II4ECLtsJ0CcDUYfR-h8pkWm1Zk7ChE-aX-aNU,55
11
+ taskforce/l2/config/config.py,sha256=W1igecQak7HiNOMk0T6Y07g6tDDypQ3ehWgIpsXhE_Y,2494
12
+ taskforce/l2/config/for-agent-moduleinfo.md,sha256=sBvvLzUEJQVXawgNSIlOOyFYs0Uq4b7OItbYFfasKF4,781
13
+ taskforce/l2/cost_logger/__init__.py,sha256=J2eqb0NP92XagSO7gIt5s3Jm_z46trkzYDRbzppU9QU,62
14
+ taskforce/l2/cost_logger/cost_logger.py,sha256=oHFrnrfxFDmFYyMX-WsMvmfGIoL_3IYKqCziStZN148,1549
15
+ taskforce/l2/cost_logger/for-agent-moduleinfo.md,sha256=ddRq_KVjbwJndkfeKluB6bfx3xnWhITc36FFRhOJZQA,595
16
+ taskforce/l2/panelist/__init__.py,sha256=NlGY0oet_C5AWApsCbkOaxvznDPwIOqKqvejKUT2M5U,55
17
+ taskforce/l2/panelist/for-agent-moduleinfo.md,sha256=_g7M5mTuqLkSlFqYbvJoB7wtR2IR_cNYOoZ0vh8rERU,582
18
+ taskforce/l2/panelist/panelist.py,sha256=rZWbcDHl6KAwTGh6ZD0R97T4g04ZjWXMbOg2ZBROTOk,1897
19
+ taskforce/l2/session_logger/__init__.py,sha256=i5R8g-8QNrQWshlGIqV91XumjZezfrZRYOJdmgtWuKQ,71
20
+ taskforce/l2/session_logger/for-agent-moduleinfo.md,sha256=l6_lgosPqp3y4beqds7U1L6S9hjGIQbhkuxcoK96GfE,611
21
+ taskforce/l2/session_logger/session_logger.py,sha256=PDIVd9WTNTqpvLq8QStPsyFFykPgVp7cL51-8Q-jtzc,1593
22
+ taskforce/l3/__init__.py,sha256=9KyOzm-QQS3MPFaP3sJQIJqUXNxfhTP5m_nLnsUn3VU,61
23
+ taskforce/l3/for-agent-layerinfo-l3.md,sha256=Vuo2ofbPbVRMKzLGwBskgZ5ckdta2mi2YRgzHbOnvIg,290
24
+ taskforce/l3/roundtable/__init__.py,sha256=9KyOzm-QQS3MPFaP3sJQIJqUXNxfhTP5m_nLnsUn3VU,61
25
+ taskforce/l3/roundtable/for-agent-moduleinfo.md,sha256=AALwBRRZc6_nvejn8XV24N4DkRmvLF_tbu-sLS7yJaI,885
26
+ taskforce/l3/roundtable/roundtable.py,sha256=DOb8aysINeQS4bp_21LW9CQAYP4GBeO-1Win-Fde2fA,3982
27
+ taskforce/l4/__init__.py,sha256=EnxeuegsZP8DT8D0Ziy36zbVvpeTUUKjP2BfAMzwSV4,65
28
+ taskforce/l4/for-agent-layerinfo-l4.md,sha256=ynFygOe89Z06cNLwOkoztW1AEr7rFUq6a_azmNAo2Mo,229
29
+ taskforce/l4/taskforce_facade/__init__.py,sha256=EnxeuegsZP8DT8D0Ziy36zbVvpeTUUKjP2BfAMzwSV4,65
30
+ taskforce/l4/taskforce_facade/for-agent-moduleinfo.md,sha256=mju3fdECZN0m0hcoIpklP37DeZlV2wkIVROovNLGIoY,711
31
+ taskforce/l4/taskforce_facade/taskforce_facade.py,sha256=6LlSMwa30NPLaknSCXuiaDM4-sTrc752P4xgs8OwemQ,1437
32
+ taskforce/mcp_wrapper/__init__.py,sha256=ethK8wvFtEwoanJ7QvREYfFkm2430FtU5SPuipmEGOw,48
33
+ taskforce/mcp_wrapper/__main__.py,sha256=U1mIekzzLhZIprEhZ7yLp_ysVnYjukAE-XLJe_8KBPg,40
34
+ taskforce/mcp_wrapper/mcp_wrapper.py,sha256=_UWYQwswwMi8n0RVrAcCS06yJJ6KfheyMQpr1cKdxoE,1875
35
+ ff_taskforce-0.1.0.dist-info/METADATA,sha256=zixX-N7SUyozOI1OMMpckDGR72vrZsgxJdVkGUI4WGQ,4712
36
+ ff_taskforce-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
37
+ ff_taskforce-0.1.0.dist-info/licenses/LICENSE,sha256=Is6uQxKak3NHGPiVNoiMJUOEmLcvt5yqetu2h3RYTlI,1079
38
+ ff_taskforce-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 taskforce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
taskforce/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .l4.taskforce_facade import Taskforce
2
+
3
+ __all__ = ['Taskforce']
@@ -0,0 +1,16 @@
1
+ # taskforce
2
+
3
+ ## l1
4
+ - schema: 데이터 모델 (ModelEntry, Opinion, IdeaPool)
5
+
6
+ ## l2
7
+ - config: .env 로드, 모델 레지스트리, caller 제외 로직
8
+ - panelist: LiteLLM 래퍼, 개별 모델 async 호출
9
+ - session_logger: 세션 전문 로그 (~/.taskforce/logs/)
10
+ - cost_logger: 비용 요약 로그 (~/.taskforce/cost_logs/)
11
+
12
+ ## l3
13
+ - roundtable: 병렬 질의 + summarizer 분류 취합
14
+
15
+ ## l4
16
+ - taskforce_facade: 최상위 퍼사드, 초기화/discuss/로그 기록
@@ -0,0 +1,3 @@
1
+ from .schema import ModelEntry, Opinion, DivergentPoint, UniquePoint, IdeaPool
2
+
3
+ __all__ = ['ModelEntry', 'Opinion', 'DivergentPoint', 'UniquePoint', 'IdeaPool']
@@ -0,0 +1,8 @@
1
+ # l1
2
+
3
+ ## schema
4
+ ModelEntry(provider: str, model_id: str, api_key: str, display_name: str)
5
+ Opinion(model_name: str, content: str)
6
+ DivergentPoint(topic: str, positions: dict[str, str])
7
+ UniquePoint(point: str, source_model: str)
8
+ IdeaPool(agenda: str, common: list[str], divergent: list[DivergentPoint], unique: list[UniquePoint])
@@ -0,0 +1,3 @@
1
+ from .schema import ModelEntry, Opinion, DivergentPoint, UniquePoint, IdeaPool
2
+
3
+ __all__ = ['ModelEntry', 'Opinion', 'DivergentPoint', 'UniquePoint', 'IdeaPool']
@@ -0,0 +1,47 @@
1
+ # Schema
2
+
3
+ pydantic 데이터 모델 정의.
4
+
5
+ ## ModelEntry
6
+
7
+ LLM 프로바이더 모델 정보.
8
+
9
+ ### Properties
10
+ provider: str # 프로바이더명 (openai, xai, anthropic, gemini)
11
+ model_id: str # LiteLLM 모델 ID
12
+ api_key: str # API 키
13
+ display_name: str # 표시용 이름
14
+
15
+ ## Opinion
16
+
17
+ 패널리스트 응답.
18
+
19
+ ### Properties
20
+ model_name: str # 응답한 모델 표시명
21
+ content: str # 응답 전문
22
+
23
+ ## DivergentPoint
24
+
25
+ 모델 간 의견이 갈리는 지점.
26
+
27
+ ### Properties
28
+ topic: str # 쟁점
29
+ positions: dict[str, str] # {model_name: 해당 입장}
30
+
31
+ ## UniquePoint
32
+
33
+ 한 모델만 제시한 고유 관점.
34
+
35
+ ### Properties
36
+ point: str # 고유 관점 내용
37
+ source_model: str # 제시한 모델명
38
+
39
+ ## IdeaPool
40
+
41
+ 라운드테이블 결과. 구조화된 아이디어 풀.
42
+
43
+ ### Properties
44
+ agenda: str # 원본 의제
45
+ common: list[str] # 다수 모델 공통 포인트
46
+ divergent: list[DivergentPoint] # 이견 지점
47
+ unique: list[UniquePoint] # 고유 관점
@@ -0,0 +1,35 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ModelEntry(BaseModel):
5
+ provider: str
6
+ model_id: str
7
+ api_key: str
8
+ display_name: str
9
+ extra_params: dict = {}
10
+
11
+
12
+ class Opinion(BaseModel):
13
+ model_name: str
14
+ content: str
15
+ cost: float = 0.0
16
+ tokens: int = 0
17
+
18
+
19
+ class DivergentPoint(BaseModel):
20
+ topic: str
21
+ positions: dict[str, str]
22
+
23
+
24
+ class UniquePoint(BaseModel):
25
+ point: str
26
+ source_model: str
27
+
28
+
29
+ class IdeaPool(BaseModel):
30
+ agenda: str
31
+ common: list[str]
32
+ divergent: list[DivergentPoint]
33
+ unique: list[UniquePoint]
34
+ total_cost: float = 0.0
35
+ total_tokens: int = 0
@@ -0,0 +1,6 @@
1
+ from .config import EnvConfig
2
+ from .panelist import Panelist
3
+ from .session_logger import SessionLogger
4
+ from .cost_logger import CostLogger
5
+
6
+ __all__ = ['EnvConfig', 'Panelist', 'SessionLogger', 'CostLogger']
@@ -0,0 +1,3 @@
1
+ from .config import EnvConfig
2
+
3
+ __all__ = ['EnvConfig']
@@ -0,0 +1,65 @@
1
+ import os
2
+
3
+ from dotenv import load_dotenv
4
+ from loguru import logger
5
+
6
+ from ...l1.schema import ModelEntry
7
+
8
+ # 환경변수 -> 패널 모델 매핑
9
+ _PANEL_REGISTRY = [
10
+ {"env_key": "OPENAI_API_KEY", "provider": "openai", "model_id": "gpt-5.4", "display_name": "GPT 5.4"},
11
+ {"env_key": "XAI_API_KEY", "provider": "xai", "model_id": "grok-4-1-fast-reasoning", "display_name": "Grok 4-1"},
12
+ {"env_key": "ANTHROPIC_API_KEY", "provider": "anthropic", "model_id": "claude-opus-4", "display_name": "Claude opus-4"},
13
+ {"env_key": "GEMINI_API_KEY", "provider": "gemini", "model_id": "gemini-2.5-pro", "display_name": "Gemini 2.5 Pro"},
14
+ ]
15
+
16
+ _SUMMARIZER_MODEL_ID = "grok-4-1-fast-non-reasoning"
17
+ _SUMMARIZER_DISPLAY_NAME = "Grok 4-1 Fast"
18
+
19
+
20
+ class EnvConfig:
21
+ def __init__(self, caller_provider: str, dotenv_path: str | None = None):
22
+ self.caller_provider = caller_provider
23
+
24
+ if dotenv_path:
25
+ load_dotenv(dotenv_path)
26
+ else:
27
+ load_dotenv()
28
+
29
+ # caller_provider 제외, 키가 있는 모델만 패널 등록
30
+ self.panel_models: list[ModelEntry] = []
31
+ for entry in _PANEL_REGISTRY:
32
+ api_key = os.getenv(entry["env_key"], "")
33
+ if not api_key:
34
+ continue
35
+ if entry["provider"] == caller_provider:
36
+ logger.debug(f"패널 제외 (caller): {entry['display_name']}")
37
+ continue
38
+ self.panel_models.append(ModelEntry(
39
+ provider=entry["provider"],
40
+ model_id=entry["model_id"],
41
+ api_key=api_key,
42
+ display_name=entry["display_name"],
43
+ extra_params=entry.get("extra_params", {}),
44
+ ))
45
+ logger.debug(f"패널 등록: {entry['display_name']}")
46
+
47
+ if not self.panel_models:
48
+ raise ValueError("활성 패널 모델이 없습니다. .env에 API 키를 설정하세요.")
49
+
50
+ # Summarizer (XAI_API_KEY 공유)
51
+ xai_key = os.getenv("XAI_API_KEY", "")
52
+ if not xai_key:
53
+ raise ValueError("XAI_API_KEY 필요 (summarizer용)")
54
+
55
+ self.summarizer_model = ModelEntry(
56
+ provider="xai",
57
+ model_id=_SUMMARIZER_MODEL_ID,
58
+ api_key=xai_key,
59
+ display_name=_SUMMARIZER_DISPLAY_NAME,
60
+ )
61
+
62
+ logger.info(f"EnvConfig 초기화: 패널 {len(self.panel_models)}개, caller={caller_provider}")
63
+
64
+ def get_active_panels(self) -> list[ModelEntry]:
65
+ return self.panel_models
@@ -0,0 +1,24 @@
1
+ # Config
2
+
3
+ .env에서 API 키를 로드하고 모델 레지스트리를 구성.
4
+
5
+ ## EnvConfig
6
+
7
+ 환경변수 기반 모델 구성. caller_provider 모델은 패널에서 제외.
8
+
9
+ ### Properties
10
+ caller_provider: str # 호출자 프로바이더
11
+ panel_models: list[ModelEntry] # 활성 패널 모델 목록
12
+ summarizer_model: ModelEntry # Summarizer 모델 (grok-4-1-fast-non-reasoning)
13
+
14
+ ### __init__
15
+ __init__(caller_provider: str, dotenv_path: str | None = None)
16
+ raise ValueError
17
+ .env 로드 후 키가 있고 caller가 아닌 모델만 패널 등록.
18
+ 활성 패널 0개이면 ValueError.
19
+ XAI_API_KEY 없으면 ValueError (summarizer 필수).
20
+
21
+ ### Methods
22
+
23
+ get_active_panels() -> list[ModelEntry]
24
+ 등록된 패널 모델 목록 반환.
@@ -0,0 +1,3 @@
1
+ from .cost_logger import CostLogger
2
+
3
+ __all__ = ['CostLogger']
@@ -0,0 +1,48 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from loguru import logger
6
+
7
+ from ...l1.schema import Opinion
8
+
9
+ _DEFAULT_DIR = Path.home() / ".taskforce" / "cost_logs"
10
+
11
+
12
+ class CostLogger:
13
+ def __init__(self, log_dir: Path | None = None):
14
+ self.log_dir = log_dir or _DEFAULT_DIR
15
+ self.log_dir.mkdir(parents=True, exist_ok=True)
16
+
17
+ def log(self, agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None:
18
+ timestamp = datetime.now().isoformat()
19
+
20
+ models = []
21
+ for op in opinions:
22
+ models.append({
23
+ "model_name": op.model_name,
24
+ "tokens": op.tokens,
25
+ "cost": op.cost,
26
+ })
27
+
28
+ total_cost = sum(op.cost for op in opinions) + summarizer_cost
29
+ total_tokens = sum(op.tokens for op in opinions) + summarizer_tokens
30
+
31
+ record = {
32
+ "timestamp": timestamp,
33
+ "agenda": agenda[:100],
34
+ "models": models,
35
+ "summarizer_cost": summarizer_cost,
36
+ "summarizer_tokens": summarizer_tokens,
37
+ "total_cost": total_cost,
38
+ "total_tokens": total_tokens,
39
+ }
40
+
41
+ # 월별 파일로 분리
42
+ month = datetime.now().strftime("%Y-%m")
43
+ filepath = self.log_dir / f"{month}.jsonl"
44
+
45
+ with open(filepath, "a", encoding="utf-8") as f:
46
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
47
+
48
+ logger.debug(f"비용 로그 기록: ${total_cost:.4f}, {total_tokens} tokens")
@@ -0,0 +1,21 @@
1
+ # CostLogger
2
+
3
+ 비용 요약 로그 기록. 모델별 tokens/cost를 JSONL로 저장.
4
+
5
+ ## CostLogger
6
+
7
+ ~/.taskforce/cost_logs/ 에 월별 JSONL 파일로 누적 기록.
8
+
9
+ ### Properties
10
+ log_dir: Path # 로그 디렉토리 경로
11
+
12
+ ### __init__
13
+ __init__(log_dir: Path | None = None)
14
+ log_dir 미지정 시 ~/.taskforce/cost_logs/ 사용.
15
+ 디렉토리 자동 생성.
16
+
17
+ ### Methods
18
+
19
+ log(agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None
20
+ 비용 레코드를 월별 파일에 append.
21
+ 모델별 tokens/cost + 총계 기록.
@@ -0,0 +1,17 @@
1
+ # l2
2
+
3
+ ## config
4
+ EnvConfig.__init__(caller_provider: str, dotenv_path: str | None = None)
5
+ EnvConfig.get_active_panels() -> list[ModelEntry]
6
+
7
+ ## panelist
8
+ Panelist.__init__(model_entry: ModelEntry)
9
+ Panelist.ask_async(prompt: str, system_msg: str = "") -> Opinion
10
+
11
+ ## session_logger
12
+ SessionLogger.__init__(log_dir: Path | None = None)
13
+ SessionLogger.log(agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path
14
+
15
+ ## cost_logger
16
+ CostLogger.__init__(log_dir: Path | None = None)
17
+ CostLogger.log(agenda: str, opinions: list[Opinion], summarizer_cost: float = 0.0, summarizer_tokens: int = 0) -> None
@@ -0,0 +1,3 @@
1
+ from .panelist import Panelist
2
+
3
+ __all__ = ['Panelist']
@@ -0,0 +1,23 @@
1
+ # Panelist
2
+
3
+ LiteLLM 래퍼. 개별 LLM 모델에 async 질의.
4
+
5
+ ## Panelist
6
+
7
+ ModelEntry를 받아 LiteLLM acompletion으로 호출.
8
+
9
+ ### Properties
10
+ model_entry: ModelEntry # 모델 정보
11
+
12
+ ### __init__
13
+ __init__(model_entry: ModelEntry)
14
+ ModelEntry 기반으로 LiteLLM 모델 ID 해석.
15
+ OpenAI는 model_id만, 나머지는 provider/model_id 형식.
16
+
17
+ ### Methods
18
+
19
+ ask_async(prompt: str, system_msg: str = "") -> Opinion
20
+ raise Exception
21
+ LiteLLM acompletion으로 모델 호출.
22
+ system_msg가 있으면 system role로 전달.
23
+ 실패 시 원본 예외 전파.
@@ -0,0 +1,52 @@
1
+ import litellm
2
+ from loguru import logger
3
+
4
+ from ...l1.schema import ModelEntry, Opinion
5
+
6
+
7
+ class Panelist:
8
+ def __init__(self, model_entry: ModelEntry):
9
+ self.model_entry = model_entry
10
+ self._litellm_model = self._resolve_model_id()
11
+
12
+ def _resolve_model_id(self) -> str:
13
+ # LiteLLM: OpenAI는 model_id만, 나머지는 provider/model_id
14
+ if self.model_entry.provider == "openai":
15
+ return self.model_entry.model_id
16
+ return f"{self.model_entry.provider}/{self.model_entry.model_id}"
17
+
18
+ async def ask_async(self, prompt: str, system_msg: str = "") -> Opinion:
19
+ messages = []
20
+ if system_msg:
21
+ messages.append({"role": "system", "content": system_msg})
22
+ messages.append({"role": "user", "content": prompt})
23
+
24
+ try:
25
+ response = await litellm.acompletion(
26
+ model=self._litellm_model,
27
+ messages=messages,
28
+ api_key=self.model_entry.api_key,
29
+ **self.model_entry.extra_params,
30
+ )
31
+ content = response.choices[0].message.content
32
+
33
+ # 비용/토큰 추출
34
+ cost = 0.0
35
+ tokens = 0
36
+ try:
37
+ cost = litellm.completion_cost(completion_response=response)
38
+ except Exception:
39
+ pass
40
+ if hasattr(response, "usage") and response.usage:
41
+ tokens = getattr(response.usage, "total_tokens", 0)
42
+
43
+ logger.debug(f"{self.model_entry.display_name} 응답: {len(content)} chars, {tokens} tokens, ${cost:.4f}")
44
+ return Opinion(
45
+ model_name=self.model_entry.display_name,
46
+ content=content,
47
+ cost=cost,
48
+ tokens=tokens,
49
+ )
50
+ except Exception as e:
51
+ logger.error(f"{self.model_entry.display_name} 호출 실패: {e}")
52
+ raise
@@ -0,0 +1,3 @@
1
+ from .session_logger import SessionLogger
2
+
3
+ __all__ = ['SessionLogger']
@@ -0,0 +1,22 @@
1
+ # SessionLogger
2
+
3
+ 세션 전문 로그 기록. prompt, response 원문을 JSONL로 저장.
4
+
5
+ ## SessionLogger
6
+
7
+ ~/.taskforce/logs/ 에 세션별 JSONL 파일 생성.
8
+
9
+ ### Properties
10
+ log_dir: Path # 로그 디렉토리 경로
11
+
12
+ ### __init__
13
+ __init__(log_dir: Path | None = None)
14
+ log_dir 미지정 시 ~/.taskforce/logs/ 사용.
15
+ 디렉토리 자동 생성.
16
+
17
+ ### Methods
18
+
19
+ log(agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path
20
+ 세션 로그 파일 생성.
21
+ request, opinion, summarizer 레코드를 JSONL로 기록.
22
+ 생성된 파일 경로 반환.
@@ -0,0 +1,48 @@
1
+ import json
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+
5
+ from loguru import logger
6
+
7
+ from ...l1.schema import Opinion
8
+
9
+ _DEFAULT_DIR = Path.home() / ".taskforce" / "logs"
10
+
11
+
12
+ class SessionLogger:
13
+ def __init__(self, log_dir: Path | None = None):
14
+ self.log_dir = log_dir or _DEFAULT_DIR
15
+ self.log_dir.mkdir(parents=True, exist_ok=True)
16
+
17
+ def log(self, agenda: str, context: str, opinions: list[Opinion], summarizer_output: str = "") -> Path:
18
+ timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
19
+ filepath = self.log_dir / f"{timestamp}.jsonl"
20
+
21
+ with open(filepath, "a", encoding="utf-8") as f:
22
+ # 의제 기록
23
+ f.write(json.dumps({
24
+ "type": "request",
25
+ "timestamp": timestamp,
26
+ "agenda": agenda,
27
+ "context": context,
28
+ }, ensure_ascii=False) + "\n")
29
+
30
+ # 각 패널 응답 기록
31
+ for op in opinions:
32
+ f.write(json.dumps({
33
+ "type": "opinion",
34
+ "model_name": op.model_name,
35
+ "content": op.content,
36
+ "tokens": op.tokens,
37
+ "cost": op.cost,
38
+ }, ensure_ascii=False) + "\n")
39
+
40
+ # summarizer 출력 기록
41
+ if summarizer_output:
42
+ f.write(json.dumps({
43
+ "type": "summarizer",
44
+ "content": summarizer_output,
45
+ }, ensure_ascii=False) + "\n")
46
+
47
+ logger.debug(f"세션 로그 기록: {filepath}")
48
+ return filepath
@@ -0,0 +1,3 @@
1
+ from .roundtable import Roundtable
2
+
3
+ __all__ = ['Roundtable']
@@ -0,0 +1,7 @@
1
+ # l3
2
+
3
+ ## roundtable
4
+ Roundtable.__init__(panelists: list[Panelist], summarizer: Panelist)
5
+ Roundtable.gather(agenda: str, context: str = "") -> list[Opinion]
6
+ Roundtable.summarize(agenda: str, opinions: list[Opinion]) -> IdeaPool
7
+ Roundtable.discuss(agenda: str, context: str = "") -> IdeaPool
@@ -0,0 +1,3 @@
1
+ from .roundtable import Roundtable
2
+
3
+ __all__ = ['Roundtable']
@@ -0,0 +1,30 @@
1
+ # Roundtable
2
+
3
+ 병렬 패널 질의 + summarizer 분류 취합.
4
+
5
+ ## Roundtable
6
+
7
+ 다수 패널리스트에 의제를 병렬 질의하고 summarizer로 분류.
8
+
9
+ ### Properties
10
+ panelists: list[Panelist] # 패널 모델 목록
11
+ summarizer: Panelist # 분류 취합용 모델
12
+
13
+ ### __init__
14
+ __init__(panelists: list[Panelist], summarizer: Panelist)
15
+ 패널과 summarizer 설정.
16
+
17
+ ### Methods
18
+
19
+ gather(agenda: str, context: str = "") -> list[Opinion]
20
+ asyncio.gather로 전체 패널 병렬 질의.
21
+ 실패한 패널은 제외, 성공분만 반환.
22
+
23
+ summarize(agenda: str, opinions: list[Opinion]) -> IdeaPool
24
+ raise ValueError
25
+ opinions를 summarizer에 전달하여 common/divergent/unique 분류.
26
+ JSON 파싱 실패 시 ValueError.
27
+
28
+ discuss(agenda: str, context: str = "") -> IdeaPool
29
+ gather + summarize 일괄 실행.
30
+ 의견 0개이면 빈 IdeaPool 반환.
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+
5
+ from loguru import logger
6
+
7
+ from ...l1.schema import Opinion, IdeaPool, DivergentPoint, UniquePoint
8
+ from ...l2.panelist import Panelist
9
+
10
+ _PANEL_SYSTEM_MSG = (
11
+ "You are an expert providing your independent analysis. "
12
+ "Be specific, thorough, and present your unique perspective."
13
+ )
14
+
15
+ _SUMMARIZER_SYSTEM_MSG = (
16
+ "You are an analyst. Given multiple expert opinions on an agenda, "
17
+ "classify the key points into three categories.\n"
18
+ "Respond ONLY in valid JSON with this exact structure:\n"
19
+ "{\n"
20
+ ' "common": ["point shared by most experts", ...],\n'
21
+ ' "divergent": [{"topic": "topic", "positions": {"Expert1": "position", "Expert2": "position"}}, ...],\n'
22
+ ' "unique": [{"point": "insight", "source_model": "ExpertName"}, ...]\n'
23
+ "}\n"
24
+ "Rules:\n"
25
+ "- common: points that 2+ experts agree on\n"
26
+ "- divergent: topics where experts have clearly different positions\n"
27
+ "- unique: points raised by only one expert\n"
28
+ "- Be thorough, do not lose information\n"
29
+ "- Respond in the same language as the opinions"
30
+ )
31
+
32
+
33
+ class Roundtable:
34
+ def __init__(self, panelists: list[Panelist], summarizer: Panelist):
35
+ self.panelists = panelists
36
+ self.summarizer = summarizer
37
+ logger.info(f"Roundtable 구성: 패널 {len(panelists)}명")
38
+
39
+ async def gather(self, agenda: str, context: str = "") -> list[Opinion]:
40
+ prompt = self._build_prompt(agenda, context)
41
+ tasks = [p.ask_async(prompt, _PANEL_SYSTEM_MSG) for p in self.panelists]
42
+ results = await asyncio.gather(*tasks, return_exceptions=True)
43
+
44
+ opinions = []
45
+ for i, result in enumerate(results):
46
+ if isinstance(result, Exception):
47
+ name = self.panelists[i].model_entry.display_name
48
+ logger.warning(f"패널 {name} 실패, 제외: {result}")
49
+ continue
50
+ opinions.append(result)
51
+
52
+ logger.info(f"의견 수집: {len(opinions)}/{len(self.panelists)}")
53
+ return opinions
54
+
55
+ async def summarize(self, agenda: str, opinions: list[Opinion]) -> IdeaPool:
56
+ opinions_text = "\n\n".join(
57
+ f"--- {op.model_name} ---\n{op.content}" for op in opinions
58
+ )
59
+ prompt = f"Agenda: {agenda}\n\nExpert Opinions:\n{opinions_text}"
60
+
61
+ result = await self.summarizer.ask_async(prompt, _SUMMARIZER_SYSTEM_MSG)
62
+ parsed = self._extract_json(result.content)
63
+
64
+ # 비용 합산: 패널 + summarizer
65
+ total_cost = sum(op.cost for op in opinions) + result.cost
66
+ total_tokens = sum(op.tokens for op in opinions) + result.tokens
67
+
68
+ return IdeaPool(
69
+ agenda=agenda,
70
+ common=parsed.get("common", []),
71
+ divergent=[DivergentPoint(**d) for d in parsed.get("divergent", [])],
72
+ unique=[UniquePoint(**u) for u in parsed.get("unique", [])],
73
+ total_cost=total_cost,
74
+ total_tokens=total_tokens,
75
+ )
76
+
77
+ async def discuss(self, agenda: str, context: str = "") -> tuple[IdeaPool, list[Opinion]]:
78
+ opinions = await self.gather(agenda, context)
79
+ if not opinions:
80
+ logger.warning("수집된 의견 없음")
81
+ pool = IdeaPool(agenda=agenda, common=[], divergent=[], unique=[])
82
+ return pool, []
83
+ pool = await self.summarize(agenda, opinions)
84
+ return pool, opinions
85
+
86
+ def _build_prompt(self, agenda: str, context: str = "") -> str:
87
+ parts = [f"Agenda: {agenda}"]
88
+ if context:
89
+ parts.insert(0, f"Context: {context}")
90
+ return "\n\n".join(parts)
91
+
92
+ def _extract_json(self, text: str) -> dict:
93
+ match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
94
+ if match:
95
+ text = match.group(1)
96
+
97
+ start = text.find("{")
98
+ end = text.rfind("}")
99
+ if start == -1 or end == -1:
100
+ raise ValueError(f"JSON 파싱 실패: {text[:200]}")
101
+
102
+ return json.loads(text[start:end + 1])
@@ -0,0 +1,3 @@
1
+ from .taskforce_facade import Taskforce
2
+
3
+ __all__ = ['Taskforce']
@@ -0,0 +1,6 @@
1
+ # l4
2
+
3
+ ## taskforce_facade
4
+ Taskforce.__init__(caller_provider: str, dotenv_path: str | None = None)
5
+ Taskforce.discuss_async(agenda: str, context: str = "") -> IdeaPool
6
+ Taskforce.discuss(agenda: str, context: str = "") -> IdeaPool
@@ -0,0 +1,3 @@
1
+ from .taskforce_facade import Taskforce
2
+
3
+ __all__ = ['Taskforce']
@@ -0,0 +1,25 @@
1
+ # Taskforce Facade
2
+
3
+ 최상위 진입점. 초기화와 discuss 호출을 단순화.
4
+
5
+ ## Taskforce
6
+
7
+ EnvConfig + Roundtable 조합을 단일 인터페이스로 노출.
8
+
9
+ ### Properties
10
+ config: EnvConfig # 환경 설정
11
+ roundtable: Roundtable # 라운드테이블 인스턴스
12
+
13
+ ### __init__
14
+ __init__(caller_provider: str, dotenv_path: str | None = None)
15
+ raise ValueError
16
+ EnvConfig 로드, Panelist/Roundtable 자동 구성.
17
+ EnvConfig 실패 시 ValueError 전파.
18
+
19
+ ### Methods
20
+
21
+ discuss_async(agenda: str, context: str = "") -> IdeaPool
22
+ async 버전. roundtable.discuss 위임.
23
+
24
+ discuss(agenda: str, context: str = "") -> IdeaPool
25
+ sync 버전. asyncio.run으로 discuss_async 실행.
@@ -0,0 +1,40 @@
1
+ import asyncio
2
+
3
+ from loguru import logger
4
+
5
+ from ...l1.schema import IdeaPool
6
+ from ...l2.config import EnvConfig
7
+ from ...l2.panelist import Panelist
8
+ from ...l2.session_logger import SessionLogger
9
+ from ...l2.cost_logger import CostLogger
10
+ from ...l3.roundtable import Roundtable
11
+
12
+
13
+ class Taskforce:
14
+ def __init__(self, caller_provider: str, dotenv_path: str | None = None):
15
+ self.config = EnvConfig(caller_provider, dotenv_path)
16
+
17
+ panelists = [Panelist(m) for m in self.config.get_active_panels()]
18
+ summarizer = Panelist(self.config.summarizer_model)
19
+ self.roundtable = Roundtable(panelists, summarizer)
20
+
21
+ self.session_logger = SessionLogger()
22
+ self.cost_logger = CostLogger()
23
+
24
+ logger.info("Taskforce 초기화 완료")
25
+
26
+ async def discuss_async(self, agenda: str, context: str = "") -> IdeaPool:
27
+ pool, opinions = await self.roundtable.discuss(agenda, context)
28
+
29
+ # 로그 기록
30
+ if opinions:
31
+ summarizer_cost = pool.total_cost - sum(op.cost for op in opinions)
32
+ summarizer_tokens = pool.total_tokens - sum(op.tokens for op in opinions)
33
+
34
+ self.session_logger.log(agenda, context, opinions)
35
+ self.cost_logger.log(agenda, opinions, summarizer_cost, summarizer_tokens)
36
+
37
+ return pool
38
+
39
+ def discuss(self, agenda: str, context: str = "") -> IdeaPool:
40
+ return asyncio.run(self.discuss_async(agenda, context))
@@ -0,0 +1,3 @@
1
+ from .mcp_wrapper import mcp
2
+
3
+ __all__ = ['mcp']
@@ -0,0 +1,3 @@
1
+ from .mcp_wrapper import mcp
2
+
3
+ mcp.run()
@@ -0,0 +1,48 @@
1
+ import os
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from ..l4.taskforce_facade import Taskforce
6
+
7
+ mcp = FastMCP("taskforce")
8
+
9
+ _tf = None
10
+
11
+
12
+ def _get_taskforce() -> Taskforce:
13
+ global _tf
14
+ if _tf is None:
15
+ caller = os.getenv("TASKFORCE_CALLER_PROVIDER", "anthropic")
16
+ _tf = Taskforce(caller_provider=caller)
17
+ return _tf
18
+
19
+
20
+ @mcp.tool()
21
+ async def roundtable_discuss(agenda: str, context: str = "") -> str:
22
+ """Roundtable: 다수의 최고 성능 LLM(GPT, Grok, Claude, Gemini)에
23
+ 동일 의제를 독립적으로 질의하고, 수집된 의견을 common/divergent/unique로
24
+ 분류하여 구조화된 JSON을 반환한다.
25
+
26
+ 유료 API를 호출하는 도구이다. 사용 전 반드시 사용자에게 의견을 묻고,
27
+ 사용자가 동의한 경우에만 호출하라.
28
+
29
+ 서로 다른 방식으로 발달한 모델들의 관점 다양성을 활용하여
30
+ robust한 아이디어 풀을 구축하는 것이 목적이다.
31
+ 일회성 외부 의견 수집 도구이므로, 양질의 결과를 위해
32
+ agenda와 context를 최대한 풍부하게 제공해야 한다.
33
+
34
+ [agenda 작성 가이드]
35
+ - 분석/검토/비교를 요청하는 명확한 질문 형태로 작성
36
+ - 검토 범위와 판단 기준을 명시
37
+ - 모호한 표현 대신 구체적인 용어 사용
38
+
39
+ [context 작성 가이드 - 풍부할수록 좋다]
40
+ input 토큰은 저렴하므로 관련 정보를 아끼지 말고 최대한 제공하라.
41
+ 패널이 충분한 맥락 없이 일반론만 답하면 가치가 없다.
42
+ 구체적 사양, 수치, 제약 조건, 배경 지식, 도메인 맥락,
43
+ 이미 결정된 사항, 실무적 제약 등
44
+ 판단에 영향을 줄 수 있는 모든 정보를 포함하라.
45
+ """
46
+ tf = _get_taskforce()
47
+ pool = await tf.discuss_async(agenda, context)
48
+ return pool.model_dump_json(indent=2)