Simon Strandgaard commited on
Commit
6369972
·
1 Parent(s): 802e7b1

snapshot of PlanExe repo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +1 -0
  2. .gitignore +4 -0
  3. llm_config.json +60 -0
  4. requirements.txt +114 -1
  5. src/__init__.py +0 -0
  6. src/assume/README.md +3 -0
  7. src/assume/__init__.py +0 -0
  8. src/assume/assumption_orchestrator.py +72 -0
  9. src/assume/distill_assumptions.py +250 -0
  10. src/assume/make_assumptions.py +358 -0
  11. src/assume/test_data/assumptions_solar_farm_in_denmark.json +42 -0
  12. src/chunk_dataframe_with_context/__init__.py +0 -0
  13. src/chunk_dataframe_with_context/chunk_dataframe_with_context.py +43 -0
  14. src/chunk_dataframe_with_context/tests/__init__.py +0 -0
  15. src/chunk_dataframe_with_context/tests/test_chunk_dataframe_with_context.py +89 -0
  16. src/expert/README.md +7 -0
  17. src/expert/__init__.py +0 -0
  18. src/expert/expert_criticism.py +205 -0
  19. src/expert/expert_finder.py +218 -0
  20. src/expert/expert_orchestrator.py +114 -0
  21. src/expert/markdown_with_criticism_from_experts.py +97 -0
  22. src/expert/pre_project_assessment.py +404 -0
  23. src/expert/test_data/solarfarm_expert_list.json +74 -0
  24. src/expert/test_data/solarfarm_swot_analysis.md +77 -0
  25. src/fiction/__init__.py +0 -0
  26. src/fiction/data/simple_fiction_prompts.jsonl +7 -0
  27. src/fiction/fiction_writer.py +133 -0
  28. src/format_json_for_use_in_query.py +46 -0
  29. src/llm_factory.py +107 -0
  30. src/plan/README.md +4 -0
  31. src/plan/__init__.py +0 -0
  32. src/plan/app_text2plan.py +332 -0
  33. src/plan/create_pitch.py +151 -0
  34. src/plan/create_project_plan.py +311 -0
  35. src/plan/create_wbs_level1.py +120 -0
  36. src/plan/create_wbs_level2.py +192 -0
  37. src/plan/create_wbs_level3.py +192 -0
  38. src/plan/data/simple_plan_prompts.jsonl +23 -0
  39. src/plan/estimate_wbs_task_durations.py +174 -0
  40. src/plan/expert_cost.py +320 -0
  41. src/plan/filenames.py +30 -0
  42. src/plan/find_plan_prompt.py +10 -0
  43. src/plan/identify_wbs_task_dependencies.py +167 -0
  44. src/plan/plan_file.py +28 -0
  45. src/plan/run_plan_pipeline.py +934 -0
  46. src/plan/speedvsdetail.py +8 -0
  47. src/prompt/__init__.py +0 -0
  48. src/prompt/prompt_catalog.py +83 -0
  49. src/prompt/test_data/prompts_simple.jsonl +2 -0
  50. src/prompt/tests/__init__.py +0 -0
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ OPENROUTER_API_KEY='sk-or-v1-YOUR_API_KEY'
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .env
2
+ venv/
3
+ run/
4
+ __pycache__/
llm_config.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "openrouter-paid-gemini-2.0-flash-001": {
3
+ "comment": "This is very fast. It's paid, so check the pricing before use.",
4
+ "class": "OpenRouter",
5
+ "arguments": {
6
+ "model": "google/gemini-2.0-flash-001",
7
+ "api_key": "${OPENROUTER_API_KEY}",
8
+ "temperature": 0.1,
9
+ "timeout": 60.0,
10
+ "is_function_calling_model": false,
11
+ "is_chat_model": true,
12
+ "max_tokens": 8192,
13
+ "max_retries": 5
14
+ }
15
+ },
16
+ "openrouter-paid-openai-gpt-4o-mini": {
17
+ "comment": "This is medium fast. It's paid, so check the pricing before use.",
18
+ "class": "OpenRouter",
19
+ "arguments": {
20
+ "model": "openai/gpt-4o-mini",
21
+ "api_key": "${OPENROUTER_API_KEY}",
22
+ "temperature": 0.1,
23
+ "timeout": 60.0,
24
+ "is_function_calling_model": false,
25
+ "is_chat_model": true,
26
+ "max_tokens": 8192,
27
+ "max_retries": 5
28
+ }
29
+ },
30
+ "ollama-llama3.1": {
31
+ "comment": "This is runs on your own computer. It's free. Requires Ollama to be installed.",
32
+ "class": "Ollama",
33
+ "arguments": {
34
+ "model": "llama3.1:latest",
35
+ "temperature": 0.5,
36
+ "request_timeout": 120.0,
37
+ "is_function_calling_model": false
38
+ }
39
+ },
40
+ "ollama-qwen2.5-coder": {
41
+ "comment": "This is runs on your own computer. It's free. Requires Ollama to be installed.",
42
+ "class": "Ollama",
43
+ "arguments": {
44
+ "model": "qwen2.5-coder:latest",
45
+ "temperature": 0.5,
46
+ "request_timeout": 120.0,
47
+ "is_function_calling_model": false
48
+ }
49
+ },
50
+ "lmstudio-qwen2.5-7b-instruct-1m": {
51
+ "comment": "This is runs on your own computer. It's free. Requires LM Studio to be installed. Great for inspecting the request/response.",
52
+ "class": "LMStudio",
53
+ "arguments": {
54
+ "model_name": "qwen2.5-7b-instruct-1m",
55
+ "temperature": 0.2,
56
+ "request_timeout": 120.0,
57
+ "is_function_calling_model": false
58
+ }
59
+ }
60
+ }
requirements.txt CHANGED
@@ -1 +1,114 @@
1
- huggingface_hub==0.25.2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==23.2.1
2
+ aiohappyeyeballs==2.4.4
3
+ aiohttp==3.11.11
4
+ aiosignal==1.3.2
5
+ annotated-types==0.7.0
6
+ anyio==4.8.0
7
+ attrs==24.3.0
8
+ audioop-lts==0.2.1
9
+ beautifulsoup4==4.12.3
10
+ certifi==2024.12.14
11
+ charset-normalizer==3.4.1
12
+ click==8.1.8
13
+ colorlog==6.9.0
14
+ dataclasses-json==0.6.7
15
+ Deprecated==1.2.15
16
+ dirtyjson==1.0.8
17
+ distro==1.9.0
18
+ fastapi==0.115.6
19
+ ffmpy==0.5.0
20
+ filelock==3.16.1
21
+ filetype==1.2.0
22
+ frozenlist==1.5.0
23
+ fsspec==2024.12.0
24
+ gradio==5.12.0
25
+ gradio_client==1.5.4
26
+ greenlet==3.1.1
27
+ h11==0.14.0
28
+ httpcore==1.0.7
29
+ httpx==0.27.2
30
+ huggingface-hub==0.27.1
31
+ idna==3.10
32
+ Jinja2==3.1.5
33
+ jiter==0.8.2
34
+ joblib==1.4.2
35
+ llama-cloud==0.1.8
36
+ llama-index==0.12.10
37
+ llama-index-agent-openai==0.4.1
38
+ llama-index-cli==0.4.0
39
+ llama-index-core==0.12.10.post1
40
+ llama-index-embeddings-openai==0.3.1
41
+ llama-index-indices-managed-llama-cloud==0.6.3
42
+ llama-index-llms-groq==0.3.1
43
+ llama-index-llms-lmstudio==0.3.0
44
+ llama-index-llms-ollama==0.5.0
45
+ llama-index-llms-openai==0.3.13
46
+ llama-index-llms-openai-like==0.3.3
47
+ llama-index-llms-openrouter==0.3.1
48
+ llama-index-llms-together==0.3.1
49
+ llama-index-multi-modal-llms-openai==0.4.2
50
+ llama-index-program-openai==0.3.1
51
+ llama-index-question-gen-openai==0.3.0
52
+ llama-index-readers-file==0.4.2
53
+ llama-index-readers-llama-parse==0.4.0
54
+ llama-parse==0.5.19
55
+ lockfile==0.12.2
56
+ luigi==3.6.0
57
+ markdown-it-py==3.0.0
58
+ MarkupSafe==2.1.5
59
+ marshmallow==3.24.2
60
+ mdurl==0.1.2
61
+ multidict==6.1.0
62
+ mypy-extensions==1.0.0
63
+ nest-asyncio==1.6.0
64
+ networkx==3.4.2
65
+ nltk==3.9.1
66
+ numpy==2.2.1
67
+ ollama==0.4.5
68
+ openai==1.59.5
69
+ orjson==3.10.14
70
+ packaging==24.2
71
+ pandas==2.2.3
72
+ pillow==11.1.0
73
+ propcache==0.2.1
74
+ pydantic==2.10.4
75
+ pydantic_core==2.27.2
76
+ pydub==0.25.1
77
+ Pygments==2.19.1
78
+ pypdf==5.1.0
79
+ python-daemon==3.1.2
80
+ python-dateutil==2.9.0.post0
81
+ python-dotenv==1.0.1
82
+ python-multipart==0.0.20
83
+ pytz==2024.2
84
+ PyYAML==6.0.2
85
+ regex==2024.11.6
86
+ requests==2.32.3
87
+ rich==13.9.4
88
+ ruff==0.9.2
89
+ safehttpx==0.1.6
90
+ safetensors==0.5.2
91
+ semantic-version==2.10.0
92
+ shellingham==1.5.4
93
+ six==1.17.0
94
+ sniffio==1.3.1
95
+ soupsieve==2.6
96
+ SQLAlchemy==2.0.36
97
+ starlette==0.41.3
98
+ striprtf==0.0.26
99
+ tenacity==8.5.0
100
+ tiktoken==0.8.0
101
+ tokenizers==0.21.0
102
+ tomlkit==0.13.2
103
+ tornado==6.4.2
104
+ tqdm==4.67.1
105
+ transformers==4.48.1
106
+ typer==0.15.1
107
+ typing-inspect==0.9.0
108
+ typing_extensions==4.12.2
109
+ tzdata==2024.2
110
+ urllib3==2.3.0
111
+ uvicorn==0.34.0
112
+ websockets==14.1
113
+ wrapt==1.17.0
114
+ yarl==1.18.3
src/__init__.py ADDED
File without changes
src/assume/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Assume
2
+
3
+ Make assumptions about the project
src/assume/__init__.py ADDED
File without changes
src/assume/assumption_orchestrator.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.assume.assumption_orchestrator
3
+ """
4
+ import logging
5
+ from llama_index.core.llms.llm import LLM
6
+ from src.assume.make_assumptions import MakeAssumptions
7
+ from src.assume.distill_assumptions import DistillAssumptions
8
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class AssumptionOrchestrator:
13
+ def __init__(self):
14
+ self.phase1_post_callback = None
15
+ self.phase2_post_callback = None
16
+ self.make_assumptions: MakeAssumptions = None
17
+ self.distill_assumptions: DistillAssumptions = None
18
+
19
+ def execute(self, llm: LLM, query: str) -> None:
20
+ logger.info("Making assumptions...")
21
+
22
+ self.make_assumptions = MakeAssumptions.execute(llm, query)
23
+ if self.phase1_post_callback:
24
+ self.phase1_post_callback(self.make_assumptions)
25
+
26
+ logger.info(f"Distilling assumptions...")
27
+
28
+ assumptions_json_string = format_json_for_use_in_query(self.make_assumptions.assumptions)
29
+
30
+ query2 = (
31
+ f"{query}\n\n"
32
+ f"assumption.json:\n{assumptions_json_string}"
33
+ )
34
+ self.distill_assumptions = DistillAssumptions.execute(llm, query2)
35
+ if self.phase2_post_callback:
36
+ self.phase2_post_callback(self.distill_assumptions)
37
+
38
+ if __name__ == "__main__":
39
+ import logging
40
+ from src.llm_factory import get_llm
41
+ from src.plan.find_plan_prompt import find_plan_prompt
42
+ import json
43
+
44
+ logging.basicConfig(
45
+ level=logging.INFO,
46
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
47
+ handlers=[
48
+ logging.StreamHandler()
49
+ ]
50
+ )
51
+
52
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
53
+
54
+ llm = get_llm("ollama-llama3.1")
55
+ # llm = get_llm("openrouter-paid-gemini-2.0-flash-001")
56
+ # llm = get_llm("deepseek-chat")
57
+
58
+ def phase1_post_callback(make_assumptions: MakeAssumptions) -> None:
59
+ count = len(make_assumptions.assumptions)
60
+ d = make_assumptions.to_dict(include_system_prompt=False, include_user_prompt=False)
61
+ pretty = json.dumps(d, indent=2)
62
+ print(f"MakeAssumptions: Made {count} assumptions:\n{pretty}")
63
+
64
+ def phase2_post_callback(distill_assumptions: DistillAssumptions) -> None:
65
+ d = distill_assumptions.to_dict(include_system_prompt=False, include_user_prompt=False)
66
+ pretty = json.dumps(d, indent=2)
67
+ print(f"DistillAssumptions:\n{pretty}")
68
+
69
+ orchestrator = AssumptionOrchestrator()
70
+ orchestrator.phase1_post_callback = phase1_post_callback
71
+ orchestrator.phase2_post_callback = phase2_post_callback
72
+ orchestrator.execute(llm, plan_prompt)
src/assume/distill_assumptions.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ From a list of verbose assumptions, distill the key assumptions, so it can fit within the LLM's token limit.
3
+
4
+ IDEA: Sometimes the input file has lots of assumptions, but the distilled has none or a few. Important assumptions are getting lost.
5
+ This problem occur with this LLM:
6
+ "openrouter-paid-gemini-2.0-flash-001"
7
+ The llama3.1 has no problems with it.
8
+
9
+ IDEA: Sometimes it recognizes that the project starts ASAP as an assumption. This is already part of the project description, this is not something new.
10
+ How do I suppress this kind of information from the output?
11
+ """
12
+ import json
13
+ import time
14
+ from datetime import datetime
15
+ import logging
16
+ from math import ceil
17
+ from uuid import uuid4
18
+ from typing import List, Optional, Any
19
+ from dataclasses import dataclass
20
+ from pydantic import BaseModel, Field
21
+ from llama_index.core.llms.llm import LLM
22
+ from llama_index.core.llms import ChatMessage, MessageRole
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class AssumptionDetails(BaseModel):
27
+ assumption_list: list[str] = Field(description="List of assumptions")
28
+
29
+ SYSTEM_PROMPT_1 = """
30
+ You are an intelligent **Planning Assistant** specializing in distilling project assumptions for efficient use by planning tools. Your primary goal is to condense a list of verbose assumptions into a concise list of key assumptions that have a significant strategic impact on planning and execution, while ensuring that all core assumptions are captured.
31
+
32
+ **Your instructions are:**
33
+
34
+ 1. **Identify All Core Assumptions with Strategic Impact:** Extract all of the most critical assumptions from the given list, focusing on assumptions that have a significant strategic impact on project planning and execution. Ensure that *all* of these types of assumptions are captured:
35
+ - Scope and deliverables
36
+ - Timeline and deadlines
37
+ - Resources needed
38
+ - External constraints
39
+ - Dependencies between tasks
40
+ - Stakeholders and their roles
41
+ - Expected outcomes and success criteria
42
+ - Financial factors (where provided)
43
+ - Operational factors
44
+
45
+ 2. **Maintain Core Details:**
46
+ * Include crucial numeric values and any specific data points stated in the original assumptions that are strategically important.
47
+ * Distill the assumptions to their core details; remove redundant words and ensure the most important aspects are maintained.
48
+
49
+ 3. **Brevity is Essential:**
50
+ * Distill each assumption into a single, short, and clear sentence. Aim for each sentence to be approximately 10-15 words, and do not exceed 17 words.
51
+ * Avoid unnecessary phrases, repetition, and filler words.
52
+ * Do not add any extra text that is not requested in the output, only return a list of distilled assumptions in JSON.
53
+
54
+ 4. **JSON Output:**
55
+ * Output the distilled assumptions into a list in JSON format.
56
+ * The key should be "assumption_list" and its value is a JSON array of strings.
57
+
58
+ 5. **Ignore:**
59
+ * Do not include any information in the response other than the distilled list of assumptions.
60
+ * Do not comment on the quality or format of the original assumptions.
61
+ * Do not explain your reasoning.
62
+ * Do not attempt to add any information that is not provided in the original list of assumptions.
63
+
64
+ **Example output:**
65
+ {
66
+ "assumption_list": [
67
+ "The project will take 3 weeks.",
68
+ "The team consists of 3 people.",
69
+ ...
70
+ ]
71
+ }
72
+ """
73
+
74
+ SYSTEM_PROMPT_2 = """
75
+ You are an intelligent **Planning Assistant** specializing in distilling project assumptions for efficient use by planning tools. Your primary goal is to condense a list of verbose assumptions into a concise list of key assumptions that are critical for pre-planning assessment, SWOT analysis, and work breakdown structure (WBS).
76
+
77
+ **Your instructions are:**
78
+
79
+ 1. **Prioritize Strategic Assumptions:**
80
+ - Extract only the most significant assumptions that have the highest impact on project planning and execution.
81
+ - Focus on assumptions that influence multiple downstream tasks and are essential for decision-making.
82
+ - Emphasize assumptions that, if incorrect, could introduce significant risks or require major project adjustments.
83
+
84
+ 2. **Limit the Number of Assumptions:**
85
+ - Provide no more than **5 key assumptions**.
86
+ - Ensure each assumption is unique and adds distinct value to the planning process.
87
+
88
+ 3. **Ensure Direct Relevance to Planning Tools:**
89
+ - The assumptions should directly support pre-planning assessment, SWOT analysis, and WBS creation.
90
+ - Consider how each assumption feeds into these specific planning activities and contributes to actionable insights.
91
+
92
+ 4. **Maintain Core Details with Strategic Focus:**
93
+ - Include crucial numeric values and specific data points from the original assumptions that are strategically important.
94
+ - Remove redundant or overlapping assumptions to ensure each one is unique and adds distinct value.
95
+
96
+ 5. **Optimize Brevity and Precision:**
97
+ - Distill each assumption into a single, short, and clear sentence.
98
+ - Aim for each sentence to be approximately 10-15 words and do not exceed 17 words.
99
+ - Use precise language to enhance clarity and avoid ambiguity.
100
+
101
+ 6. **JSON Output:**
102
+ - Output the distilled assumptions into a list in JSON format.
103
+ - The key should be "assumption_list" and its value is a JSON array of strings.
104
+
105
+ 7. **Ignore:**
106
+ - Do not include any information in the response other than the distilled list of assumptions.
107
+ - Do not comment on the quality or format of the original assumptions.
108
+ - Do not explain your reasoning.
109
+ - Do not attempt to add any information that is not provided in the original list of assumptions.
110
+
111
+ **Example output:**
112
+ {
113
+ "assumption_list": [
114
+ "The project will take 3 weeks.",
115
+ "The team consists of 3 people.",
116
+ ...
117
+ ]
118
+ }
119
+ """
120
+
121
+ SYSTEM_PROMPT = SYSTEM_PROMPT_1
122
+
123
+ @dataclass
124
+ class DistillAssumptions:
125
+ system_prompt: Optional[str]
126
+ user_prompt: str
127
+ response: dict
128
+ metadata: dict
129
+
130
+ @classmethod
131
+ def execute(cls, llm: LLM, user_prompt: str, **kwargs: Any) -> 'DistillAssumptions':
132
+ """
133
+ Invoke LLM with a bunch of assumptions and distill them.
134
+ """
135
+ if not isinstance(llm, LLM):
136
+ raise ValueError("Invalid LLM instance.")
137
+ if not isinstance(user_prompt, str):
138
+ raise ValueError("Invalid query.")
139
+
140
+ # Obtain the current year as a string, eg. "1984"
141
+ current_year_int = datetime.now().year
142
+ current_year = str(current_year_int)
143
+
144
+ # Replace the placeholder in the system prompt with the current year
145
+ system_prompt = SYSTEM_PROMPT.strip()
146
+ system_prompt = system_prompt.replace("CURRENT_YEAR_PLACEHOLDER", current_year)
147
+
148
+ default_args = {
149
+ 'system_prompt': system_prompt
150
+ }
151
+ default_args.update(kwargs)
152
+
153
+ system_prompt = default_args.get('system_prompt')
154
+ logger.debug(f"System Prompt:\n{system_prompt}")
155
+ if system_prompt and not isinstance(system_prompt, str):
156
+ raise ValueError("Invalid system prompt.")
157
+
158
+ chat_message_list1 = []
159
+ if system_prompt:
160
+ chat_message_list1.append(
161
+ ChatMessage(
162
+ role=MessageRole.SYSTEM,
163
+ content=system_prompt,
164
+ )
165
+ )
166
+
167
+ logger.debug(f"User Prompt:\n{user_prompt}")
168
+ chat_message_user = ChatMessage(
169
+ role=MessageRole.USER,
170
+ content=user_prompt,
171
+ )
172
+ chat_message_list1.append(chat_message_user)
173
+
174
+ sllm = llm.as_structured_llm(AssumptionDetails)
175
+
176
+ logger.debug("Starting LLM chat interaction.")
177
+ start_time = time.perf_counter()
178
+ chat_response1 = sllm.chat(chat_message_list1)
179
+ end_time = time.perf_counter()
180
+ duration = int(ceil(end_time - start_time))
181
+ response_byte_count = len(chat_response1.message.content.encode('utf-8'))
182
+ logger.info(f"LLM chat interaction completed in {duration} seconds. Response byte count: {response_byte_count}")
183
+
184
+ metadata = dict(llm.metadata)
185
+ metadata["llm_classname"] = llm.class_name()
186
+ metadata["duration"] = duration
187
+ metadata["response_byte_count"] = response_byte_count
188
+
189
+ try:
190
+ json_response = json.loads(chat_response1.message.content)
191
+ except json.JSONDecodeError as e:
192
+ logger.error("Failed to parse LLM response as JSON.", exc_info=True)
193
+ raise ValueError("Invalid JSON response from LLM.") from e
194
+
195
+ result = DistillAssumptions(
196
+ system_prompt=system_prompt,
197
+ user_prompt=user_prompt,
198
+ response=json_response,
199
+ metadata=metadata,
200
+ )
201
+ logger.debug("DistillAssumptions instance created successfully.")
202
+ return result
203
+
204
+ def to_dict(self, include_metadata=True, include_system_prompt=True, include_user_prompt=True) -> dict:
205
+ d = self.response.copy()
206
+ if include_metadata:
207
+ d['metadata'] = self.metadata
208
+ if include_system_prompt:
209
+ d['system_prompt'] = self.system_prompt
210
+ if include_user_prompt:
211
+ d['user_prompt'] = self.user_prompt
212
+ return d
213
+
214
+ def save_raw(self, file_path: str) -> None:
215
+ with open(file_path, 'w') as f:
216
+ f.write(json.dumps(self.to_dict(), indent=2))
217
+
218
+ if __name__ == "__main__":
219
+ import os
220
+ import logging
221
+ from src.llm_factory import get_llm
222
+
223
+ logging.basicConfig(
224
+ level=logging.DEBUG,
225
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
226
+ handlers=[
227
+ logging.StreamHandler()
228
+ ]
229
+ )
230
+
231
+ path_to_assumptions_file = os.path.join(os.path.dirname(__file__), 'test_data', 'assumptions_solar_farm_in_denmark.json')
232
+ with open(path_to_assumptions_file, 'r', encoding='utf-8') as f:
233
+ assumptions_raw_data = f.read()
234
+
235
+ plan_prompt = "Establish a solar farm in Denmark."
236
+ query = (
237
+ f"{plan_prompt}\n\n"
238
+ "Today's date:\n2025-Jan-26\n\n"
239
+ "Project start ASAP\n\n"
240
+ f"assumption.json:\n{assumptions_raw_data}"
241
+ )
242
+
243
+ llm = get_llm("ollama-llama3.1")
244
+ # llm = get_llm("deepseek-chat", max_tokens=8192)
245
+
246
+ print(f"Query: {query}")
247
+ result = DistillAssumptions.execute(llm, query)
248
+
249
+ print("\n\nResponse:")
250
+ print(json.dumps(result.to_dict(include_system_prompt=False, include_user_prompt=False), indent=2))
src/assume/make_assumptions.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Analyze a vague description, generate relevant questions for clarification, make reasonable assumptions where necessary.
3
+
4
+ PROMPT> python -m src.assume.make_assumptions
5
+ """
6
+ import json
7
+ import time
8
+ from datetime import datetime
9
+ import logging
10
+ from math import ceil
11
+ from uuid import uuid4
12
+ from typing import List, Optional, Any
13
+ from dataclasses import dataclass
14
+ from pydantic import BaseModel, Field
15
+ from llama_index.core.llms.llm import LLM
16
+ from llama_index.core.llms import ChatMessage, MessageRole
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class QuestionAssumptionItem(BaseModel):
21
+ item_index: int = Field(description="Index in the list")
22
+ question: str = Field(description="Question to clarify and refine the user's description")
23
+ assumptions: str = Field(description="Reasonable assumptions made to fill in the gaps or missing details in the user's description.")
24
+ assessments: str = Field(description="Detailed information about the assessments, including key findings and recommendations. *max 3 assessments*.")
25
+
26
+ class ExpertDetails(BaseModel):
27
+ question_assumption_list: list[QuestionAssumptionItem] = Field(description="Questions and assumptions")
28
+
29
+ SYSTEM_PROMPT_1 = """
30
+ You are an intelligent **Planning Assistant** designed to help users develop detailed plans from vague or high-level descriptions.
31
+
32
+ **Your primary tasks are to:**
33
+
34
+ 1. **Identify Potential Questions:**
35
+ - **Analyze the provided description.**
36
+ - **List exactly eight relevant questions** that need to be answered to clarify the requirements, scope, and objectives of the project or task.
37
+ - **Each question must correspond to one of the eight critical areas** to ensure comprehensive and focused planning.
38
+
39
+ 2. **Make Reasonable Assumptions:**
40
+ - **For any information that is unclear, incomplete, or missing from the description, make logical and reasonable assumptions.**
41
+ - **Clearly label these as assumptions** to distinguish them from the user's original input.
42
+ - **Each assumption must directly correspond to its respective question,** providing clear and detailed guidance for planning without unnecessary complexity.
43
+ - **Ensure all assumptions are realistic and feasible** based on industry standards and practical considerations.
44
+
45
+ 3. **Conduct Assessments:**
46
+ - **Perform evaluations** based on the identified questions and assumptions.
47
+ - **Provide insights** into potential risks, feasibility, environmental impact, financial viability, and other relevant factors.
48
+ - **Ensure exactly eight assessments** are provided, each corresponding to one of the eight critical areas of the project.
49
+ - **Each assessment must include:**
50
+ - **Title:** A concise title for the assessment (e.g., Risk Assessment).
51
+ - **Description:** A brief overview of the assessment focus.
52
+ - **Details:** Specific insights, including likelihood, impact, and mitigation strategies.
53
+
54
+ **Guidelines:**
55
+
56
+ - **Clarity & Precision:** Ensure that all questions, assumptions, and assessments are clear, relevant, and aimed at uncovering essential details that will aid in planning.
57
+
58
+ - **Comprehensive Coverage:** Address the following eight critical areas:
59
+ 1. **Funding & Budget:** Sources, allocation, and financial planning.
60
+ 2. **Timeline & Milestones:** Project phases, deadlines, and key milestones.
61
+ 3. **Resources & Personnel:** Required materials, technologies, and team members.
62
+ 4. **Governance & Regulations:** Rules, policies, and compliance requirements.
63
+ 5. **Safety & Risk Management:** Potential risks, safety measures, and contingency plans.
64
+ 6. **Environmental Impact:** Sustainability practices and environmental considerations.
65
+ 7. **Stakeholder Involvement:** Key stakeholders, their roles, and communication strategies.
66
+ 8. **Operational Systems:** Essential systems for functionality (e.g., power, water, air).
67
+
68
+ - **Logical Assumptions:** Base all assumptions on common-sense reasoning, industry benchmarks, and any implicit information present in the description. Avoid introducing unrelated or speculative elements.
69
+
70
+ - **Realism and Feasibility:** Ensure that all assumptions are grounded in realistic scenarios by referencing industry benchmarks, historical data, and practical constraints. Avoid speculative figures unless explicitly justified by the project context.
71
+
72
+ - **Alignment:** Ensure each assumption is directly tied to its corresponding question, providing a coherent and logical foundation for planning.
73
+
74
+ - **Neutral Tone:** Maintain an objective and neutral tone, avoiding any bias or subjective opinions.
75
+
76
+ - **Conciseness:** Keep questions, assumptions, and assessments concise and to the point, ensuring they are easily understandable while still being sufficiently detailed.
77
+
78
+ - **Strict Item Limit:** Do not exceed eight items in each section. If the content naturally exceeds this limit, prioritize the most critical aspects and omit less essential details.
79
+ """
80
+
81
+ SYSTEM_PROMPT_2 = """
82
+ You are an expert **Planning Assistant** designed to transform vague descriptions into detailed, actionable plans. Your process is rigorous, structured, and ensures comprehensive coverage across all critical project areas.
83
+
84
+ **Your primary tasks are to perform the following in a strictly ordered sequence:**
85
+
86
+ 1. **Clarify Requirements with Focused Questions:**
87
+ - **Analyze the provided description** to identify its core objectives and constraints.
88
+ - **Generate exactly eight (8) targeted questions** designed to elicit essential details necessary for planning.
89
+ - **Each question MUST directly address one of the eight (8) critical planning areas** listed below, ensuring no area is overlooked.
90
+ - **Questions should be concise, specific, and directly related to the provided description.** Avoid overly generic or broad questions.
91
+ - **Output:** Present each question with an `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
92
+
93
+ 2. **Formulate Specific and Justifiable Assumptions:**
94
+ - **For every question posed, formulate a corresponding assumption.** These assumptions should bridge any gaps in the provided description and be directly related to the respective question.
95
+ - **Each assumption MUST be realistic, feasible, and based on industry benchmarks or common sense.** Justify each assumption briefly, referencing industry standards or practical considerations where applicable.
96
+ - **Label each assumption as "Assumption:"** to clearly distinguish it from user-provided information.
97
+ - **Output:** Present each assumption with a matching `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
98
+
99
+ 3. **Provide Balanced and Actionable Assessments:**
100
+ - **For every question and assumption**, conduct a comprehensive evaluation, analyzing its implications, including potential benefits, risks, and opportunities.
101
+ - **Provide exactly eight (8) assessments**, each directly linked to one question and assumption, and covering one of the Critical Planning Areas.
102
+ - **Each assessment MUST be a single string** containing:
103
+ - A concise `Title:` (e.g., "Financial Feasibility Assessment").
104
+ - A brief `Description:` of the assessment's focus.
105
+ - `Details:` Specific insights into potential risks, impacts, mitigation strategies, potential benefits, and opportunities. Focus on actionable intelligence that can drive planning decisions. Include quantifiable metrics where applicable.
106
+ - **Output:** Present each assessment with a matching `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
107
+
108
+ **Critical Planning Areas (MUST be covered by one question, assumption, and assessment each):**
109
+
110
+ * Funding & Budget
111
+ * Timeline & Milestones
112
+ * Resources & Personnel
113
+ * Governance & Regulations
114
+ * Safety & Risk Management
115
+ * Environmental Impact
116
+ * Stakeholder Involvement
117
+ * Operational Systems
118
+
119
+ **Guidelines (Strictly Follow):**
120
+
121
+ * **Strict Ordering:** Follow the sequence of tasks (questions, assumptions, assessments) and output the results in the same order.
122
+ * **Strict Item Limit:** Do not exceed eight items in each section. If the content naturally exceeds this limit, prioritize the most critical aspects and omit less essential details.
123
+ * **Direct Correspondence:** Maintain a one-to-one relationship between each question, assumption, and assessment.
124
+ * **Realism and Feasibility:** Ensure assumptions are realistic, justifiable, and based on real-world considerations.
125
+ * **Do not reference any item by index (e.g., "Assumption: 3.2").** The `item_index` is solely for output formatting.
126
+ * **Balanced Insights:** Assessments should provide a balanced perspective, including potential benefits, opportunities, risks, and actionable mitigation strategies.
127
+ * **Neutral Tone:** Maintain an objective, unbiased, and professional tone.
128
+ * **Conciseness:** Be concise and direct. Prioritize the most critical information.
129
+ * **No Exceeding Item Limit:** Strictly adhere to the 8-item limit for each task.
130
+ * **Explicit Labeling:** All assumptions must be explicitly labeled with the prefix "Assumption:".
131
+ * **Quantifiable Metrics:** Include specific numbers, measurements, or metrics in assumptions and assessments whenever possible to enhance precision.
132
+ * **Justifications:** Briefly justify assumptions using common sense, industry standards, or practical considerations.
133
+ * **Example of Assessment Output:**
134
+ ```
135
+ Title: Financial Feasibility Assessment
136
+ Description: Evaluation of the project's financial viability.
137
+ Details: Funding will come from government grants and private investors. The project has a high chance of success.
138
+ ```
139
+ """
140
+
141
+ SYSTEM_PROMPT_3 = """
142
+ You are an expert **Planning Assistant** designed to transform vague descriptions into detailed, actionable plans. Your process is rigorous, structured, and ensures comprehensive coverage across all critical project areas.
143
+
144
+ **Your primary tasks are to perform the following in a strictly ordered sequence:**
145
+
146
+ 1. **Clarify Requirements with Focused Questions:**
147
+ - **Analyze the provided description** to identify its core objectives and constraints.
148
+ - **Generate exactly eight (8) targeted questions** designed to elicit essential details necessary for planning.
149
+ - **Each question MUST directly address one of the eight (8) Critical Planning Areas** listed below, ensuring no area is overlooked.
150
+ - **Questions should be concise, specific, and directly related to the provided description.** Avoid overly generic or broad questions.
151
+ - **Output:** Present each question with an `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
152
+
153
+ 2. **Formulate Specific and Justifiable Assumptions:**
154
+ - **For every question posed, formulate a corresponding assumption.** These assumptions should bridge any gaps in the provided description and be directly related to the respective question.
155
+ - **Each assumption MUST be realistic, feasible, and based on industry benchmarks or common sense.** Justify each assumption briefly, referencing industry standards or practical considerations where applicable.
156
+ - **Label each assumption as "Assumption:"** to clearly distinguish it from user-provided information.
157
+ - **Output:** Present each assumption with a matching `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
158
+
159
+ 3. **Provide Balanced and Actionable Assessments:**
160
+ - **For every question and assumption**, conduct a comprehensive evaluation, analyzing its implications, including potential benefits, risks, and opportunities.
161
+ - **Provide exactly eight (8) assessments**, each directly linked to one question and assumption, and covering one of the Critical Planning Areas.
162
+ - **Each assessment MUST be a single string** containing:
163
+ - A concise `Title:` (e.g., "Financial Feasibility Assessment").
164
+ - A brief `Description:` of the assessment's focus.
165
+ - `Details:` Specific insights into potential risks, impacts, mitigation strategies, potential benefits, and opportunities. Focus on actionable intelligence that can drive planning decisions. Include quantifiable metrics where applicable.
166
+ - **Output:** Present each assessment with a matching `item_index` (e.g., `item_index: 1`). The `item_index` is solely for output formatting and should *not* be used to reference other parts of your response.
167
+
168
+ **Critical Planning Areas (MUST be covered by one question, assumption, and assessment each):**
169
+
170
+ * Funding & Budget
171
+ * Timeline & Milestones
172
+ * Resources & Personnel
173
+ * Governance & Regulations
174
+ * Safety & Risk Management
175
+ * Environmental Impact
176
+ * Stakeholder Involvement
177
+ * Operational Systems
178
+
179
+ **Output Format:**
180
+
181
+ The output must be a JSON object with two keys:
182
+
183
+ 1. `"question_assumption_list"`: An array of exactly eight objects, each containing:
184
+ - `item_index`: Integer from 1 to 8.
185
+ - `question`: String.
186
+ - `assumptions`: String, starting with "Assumption:".
187
+ - `assessments`: String containing Title, Description, and Details.
188
+
189
+ 2. `"metadata"`: An object containing relevant metadata about the response.
190
+
191
+ **Example JSON Output:**
192
+
193
+ {
194
+ "question_assumption_list": [
195
+ {
196
+ "item_index": 1,
197
+ "question": "What is the size of the square and the yellow ball?",
198
+ "assumptions": "Assumption: The square has a side length of 500 pixels. The yellow ball has a diameter of 50 pixels.",
199
+ "assessments": "Title: Collision Detection Assessment\nDescription: Evaluation of collision between the ball and the square.\nDetails: If the ball's center x-coordinate is less than or equal to the square's left edge, or greater than or equal to the square's right edge, the ball will bounce back. Similarly, if the ball's center y-coordinate is less than or equal to the square's top edge, or greater than or equal to the square's bottom edge, the ball will bounce up or down."
200
+ },
201
+ // ... seven more items
202
+ ]
203
+ }
204
+ """
205
+
206
+ SYSTEM_PROMPT = SYSTEM_PROMPT_3
207
+
208
+ @dataclass
209
+ class MakeAssumptions:
210
+ system_prompt: Optional[str]
211
+ user_prompt: str
212
+ response: dict
213
+ metadata: dict
214
+ assumptions: list
215
+
216
+ @classmethod
217
+ def execute(cls, llm: LLM, user_prompt: str, **kwargs: Any) -> 'MakeAssumptions':
218
+ """
219
+ Invoke LLM and make assumptions based on the user prompt.
220
+ """
221
+ if not isinstance(llm, LLM):
222
+ raise ValueError("Invalid LLM instance.")
223
+ if not isinstance(user_prompt, str):
224
+ raise ValueError("Invalid query.")
225
+
226
+ # Obtain the current year as a string, eg. "1984"
227
+ current_year_int = datetime.now().year
228
+ current_year = str(current_year_int)
229
+
230
+ # Replace the placeholder in the system prompt with the current year
231
+ system_prompt = SYSTEM_PROMPT.strip()
232
+ system_prompt = system_prompt.replace("CURRENT_YEAR_PLACEHOLDER", current_year)
233
+
234
+ default_args = {
235
+ 'system_prompt': system_prompt
236
+ }
237
+ default_args.update(kwargs)
238
+
239
+ system_prompt = default_args.get('system_prompt')
240
+ logger.debug(f"System Prompt:\n{system_prompt}")
241
+ if system_prompt and not isinstance(system_prompt, str):
242
+ raise ValueError("Invalid system prompt.")
243
+
244
+ chat_message_list1 = []
245
+ if system_prompt:
246
+ chat_message_list1.append(
247
+ ChatMessage(
248
+ role=MessageRole.SYSTEM,
249
+ content=system_prompt,
250
+ )
251
+ )
252
+
253
+ logger.debug(f"User Prompt:\n{user_prompt}")
254
+ chat_message_user = ChatMessage(
255
+ role=MessageRole.USER,
256
+ content=user_prompt,
257
+ )
258
+ chat_message_list1.append(chat_message_user)
259
+
260
+ sllm = llm.as_structured_llm(ExpertDetails)
261
+
262
+ logger.debug("Starting LLM chat interaction.")
263
+ start_time = time.perf_counter()
264
+ try:
265
+ chat_response1 = sllm.chat(chat_message_list1)
266
+ except Exception as e:
267
+ logger.debug(f"LLM chat interaction failed: {e}")
268
+ logger.error("LLM chat interaction failed.", exc_info=True)
269
+ raise ValueError("LLM chat interaction failed.") from e
270
+ end_time = time.perf_counter()
271
+ duration = int(ceil(end_time - start_time))
272
+ response_byte_count = len(chat_response1.message.content.encode('utf-8'))
273
+ logger.info(f"LLM chat interaction completed in {duration} seconds. Response byte count: {response_byte_count}")
274
+
275
+ metadata = dict(llm.metadata)
276
+ metadata["llm_classname"] = llm.class_name()
277
+ metadata["duration"] = duration
278
+ metadata["response_byte_count"] = response_byte_count
279
+
280
+ try:
281
+ json_response = json.loads(chat_response1.message.content)
282
+ except json.JSONDecodeError as e:
283
+ logger.error("Failed to parse LLM response as JSON.", exc_info=True)
284
+ raise ValueError("Invalid JSON response from LLM.") from e
285
+
286
+ # Cleanup the json response from the LLM model.
287
+ assumption_list = []
288
+ for item in json_response['question_assumption_list']:
289
+ question = item.get('question', '')
290
+ assumptions = item.get('assumptions', '')
291
+ assessments = item.get('assessments', '')
292
+
293
+ assumption_item = {
294
+ "question": question,
295
+ "assumptions": assumptions,
296
+ "assessments": assessments
297
+ }
298
+ assumption_list.append(assumption_item)
299
+
300
+ result = MakeAssumptions(
301
+ system_prompt=system_prompt,
302
+ user_prompt=user_prompt,
303
+ response=json_response,
304
+ metadata=metadata,
305
+ assumptions=assumption_list
306
+ )
307
+ logger.debug("MakeAssumptions instance created successfully.")
308
+ return result
309
+
310
+ def to_dict(self, include_metadata=True, include_system_prompt=True, include_user_prompt=True) -> dict:
311
+ d = self.response.copy()
312
+ if include_metadata:
313
+ d['metadata'] = self.metadata
314
+ if include_system_prompt:
315
+ d['system_prompt'] = self.system_prompt
316
+ if include_user_prompt:
317
+ d['user_prompt'] = self.user_prompt
318
+ return d
319
+
320
+ def save_raw(self, file_path: str) -> None:
321
+ with open(file_path, 'w') as f:
322
+ f.write(json.dumps(self.to_dict(), indent=2))
323
+
324
+ def save_assumptions(self, file_path: str) -> None:
325
+ with open(file_path, 'w') as f:
326
+ f.write(json.dumps(self.assumptions, indent=2))
327
+
328
+ if __name__ == "__main__":
329
+ import logging
330
+ from src.llm_factory import get_llm
331
+ from src.plan.find_plan_prompt import find_plan_prompt
332
+
333
+ logging.basicConfig(
334
+ level=logging.DEBUG,
335
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
336
+ handlers=[
337
+ logging.StreamHandler()
338
+ ]
339
+ )
340
+
341
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
342
+ query = (
343
+ f"{plan_prompt}\n\n"
344
+ "Today's date:\n2025-Jan-26\n\n"
345
+ "Project start ASAP"
346
+ )
347
+
348
+ llm = get_llm("ollama-llama3.1")
349
+ # llm = get_llm("deepseek-chat", max_tokens=8192)
350
+
351
+ print(f"Query: {query}")
352
+ result = MakeAssumptions.execute(llm, query)
353
+
354
+ print("\n\nResponse:")
355
+ print(json.dumps(result.to_dict(include_system_prompt=False, include_user_prompt=False), indent=2))
356
+
357
+ print("\n\nAssumptions:")
358
+ print(json.dumps(result.assumptions, indent=2))
src/assume/test_data/assumptions_solar_farm_in_denmark.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "question": "What is the target capacity of the solar farm in megawatts?",
4
+ "assumptions": "Assumption: The project will aim for a moderate-sized solar farm with an initial capacity of 50 MW, taking into account Denmark's renewable energy goals and available land.",
5
+ "assessments": "Title: Financial Feasibility Assessment\nDescription: Evaluation of the financial viability of the solar farm.\nDetails: With an initial investment of approximately DKK 375 million (\u20ac50 million), the project can expect a return on investment of around 7-8% per annum, considering Denmark's feed-in tariffs and tax incentives. However, this may be affected by fluctuations in global panel prices and local labor costs."
6
+ },
7
+ {
8
+ "question": "What is the expected timeline for the solar farm's construction?",
9
+ "assumptions": "Assumption: The project will adhere to Denmark's renewable energy regulations and guidelines, requiring a minimum of 6 months for environmental impact assessments and permitting.",
10
+ "assessments": "Title: Timeline & Milestones Assessment\nDescription: Evaluation of the project schedule.\nDetails: With a team of experienced engineers and technicians, construction can be completed within 12-14 months. However, this timeline may be impacted by weather conditions, equipment delivery delays, or unforeseen site-specific challenges."
11
+ },
12
+ {
13
+ "question": "What is the estimated workforce required for the solar farm's construction?",
14
+ "assumptions": "Assumption: The project will follow industry best practices and hire local labor, taking into account Denmark's high labor costs.",
15
+ "assessments": "Title: Resources & Personnel Assessment\nDescription: Evaluation of the human resources needed.\nDetails: With an estimated 50-60 workers required for peak construction periods, the project can expect to incur a significant labor cost. To mitigate this, local partnerships and training programs will be explored to ensure a smooth transition of skills."
16
+ },
17
+ {
18
+ "question": "What are the relevant regulations and permits required for the solar farm?",
19
+ "assumptions": "Assumption: The project will comply with Denmark's Renewable Energy Act (RE-Act) and adhere to EU directives on environmental impact assessments.",
20
+ "assessments": "Title: Governance & Regulations Assessment\nDescription: Evaluation of regulatory compliance.\nDetails: The project must obtain necessary permits from the Danish Environmental Protection Agency, including a permit for the solar farm's construction and operation. Failure to comply with regulations may result in fines or even project cancellation."
21
+ },
22
+ {
23
+ "question": "What are the potential environmental impacts of the solar farm?",
24
+ "assumptions": "Assumption: The project will prioritize sustainability and minimize environmental footprint, adhering to Denmark's green energy policies.",
25
+ "assessments": "Title: Environmental Impact Assessment\nDescription: Evaluation of the solar farm's ecological impact.\nDetails: While the solar farm itself has a minimal carbon footprint, potential environmental concerns include habitat disruption and visual pollution. To mitigate these effects, the project will work with local stakeholders to ensure that any negative impacts are minimized."
26
+ },
27
+ {
28
+ "question": "How will the solar farm be connected to the grid?",
29
+ "assumptions": "Assumption: The project will utilize Denmark's existing high-voltage transmission infrastructure and adhere to EU directives on grid connection.",
30
+ "assessments": "Title: Grid Connection Assessment\nDescription: Evaluation of the solar farm's integration with the national grid.\nDetails: With a 50 MW capacity, the solar farm will require a dedicated 132 kV line from the nearest substation. This will be designed and constructed in collaboration with Energinet.dk to ensure seamless integration."
31
+ },
32
+ {
33
+ "question": "What is the expected level of stakeholder engagement?",
34
+ "assumptions": "Assumption: The project will maintain open communication channels with local communities, ensuring their concerns are addressed.",
35
+ "assessments": "Title: Stakeholder Involvement Assessment\nDescription: Evaluation of community outreach and engagement.\nDetails: Regular public meetings and information sessions will be held to keep stakeholders informed about the project's progress. This includes partnering with local organizations to address any environmental or social concerns."
36
+ },
37
+ {
38
+ "question": "What are the operational systems required for the solar farm?",
39
+ "assumptions": "Assumption: The project will utilize industry-standard SCADA systems and adhere to Denmark's energy management guidelines.",
40
+ "assessments": "Title: Operational Systems Assessment\nDescription: Evaluation of the solar farm's operational infrastructure.\nDetails: With a focus on efficiency and reliability, the project will implement an advanced SCADA system for real-time monitoring and control. This will also enable remote maintenance and troubleshooting."
41
+ }
42
+ ]
src/chunk_dataframe_with_context/__init__.py ADDED
File without changes
src/chunk_dataframe_with_context/chunk_dataframe_with_context.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from typing import Generator, Tuple
3
+
4
+ def chunk_dataframe_with_context(
5
+ df: pd.DataFrame,
6
+ chunk_size: int = 10,
7
+ overlap: int = 3
8
+ ) -> Generator[Tuple[pd.DataFrame, pd.DataFrame], None, None]:
9
+ """
10
+ Chunk the DataFrame into overlapping segments. For each core chunk,
11
+ include 'overlap' rows before and after as additional context.
12
+
13
+ Yields:
14
+ (core_df, extended_df):
15
+ core_df = The rows we actually want to process in this chunk
16
+ extended_df = core_df + overlap context before and after
17
+ """
18
+ start = 0
19
+ total_rows = len(df)
20
+
21
+ while start < total_rows:
22
+ # Determine the start/end for the core chunk
23
+ core_start = start
24
+ core_end = min(start + chunk_size, total_rows)
25
+
26
+ # Determine the start/end for the extended chunk
27
+ extended_start = max(0, core_start - overlap)
28
+ extended_end = min(core_end + overlap, total_rows)
29
+
30
+ core_df = df.iloc[core_start:core_end]
31
+ extended_df = df.iloc[extended_start:extended_end]
32
+
33
+ yield core_df, extended_df
34
+
35
+ start += chunk_size
36
+
37
+ if __name__ == "__main__":
38
+ df = pd.DataFrame({
39
+ "Task ID": range(1, 26),
40
+ "Description": [f"Task {i}" for i in range(1, 26)]
41
+ })
42
+ for i, (core_df, extended_df) in enumerate(chunk_dataframe_with_context(df, chunk_size=5, overlap=2)):
43
+ print(f"CHUNK {i} - Core:\n{core_df}\n\nContext:\n{extended_df}\n{'-'*40}")
src/chunk_dataframe_with_context/tests/__init__.py ADDED
File without changes
src/chunk_dataframe_with_context/tests/test_chunk_dataframe_with_context.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import pandas as pd
3
+ from ..chunk_dataframe_with_context import chunk_dataframe_with_context
4
+
5
+ class TestChunkDataFrameWithContext(unittest.TestCase):
6
+ def test_empty(self):
7
+ # Arrange
8
+ df = pd.DataFrame()
9
+ df["Task ID"] = []
10
+ df["Description"] = []
11
+
12
+ # Act
13
+ chunk_size = 5
14
+ overlap = 2
15
+ results = list(chunk_dataframe_with_context(df, chunk_size=chunk_size, overlap=overlap))
16
+
17
+ # Assert
18
+ # The number of chunks expected = ceil(0 / 5) = 0
19
+ self.assertEqual(len(results), 0)
20
+
21
+ def test_one_chunk_no_overlap(self):
22
+ # Arrange
23
+ df = pd.DataFrame({
24
+ "Task ID": range(1, 4),
25
+ "Description": [f"Task {i}" for i in range(1, 4)]
26
+ })
27
+
28
+ # Act
29
+ chunk_size = 5
30
+ overlap = 2
31
+ results = list(chunk_dataframe_with_context(df, chunk_size=chunk_size, overlap=overlap))
32
+
33
+ # Assert
34
+ # The number of chunks expected = ceil(3 / 5) = 1
35
+ self.assertEqual(len(results), 1)
36
+
37
+ # Check the only chunk
38
+ first_core, first_extended = results[0]
39
+ self.assertEqual(first_core.iloc[0]["Task ID"], 1)
40
+ self.assertEqual(first_core.iloc[-1]["Task ID"], 3)
41
+ self.assertEqual(first_extended.iloc[0]["Task ID"], 1)
42
+ self.assertEqual(first_extended.iloc[-1]["Task ID"], 3)
43
+
44
+ def test_multiple_chunks(self):
45
+ # Arrange
46
+ # Create a sample DataFrame of 25 rows
47
+ df = pd.DataFrame({
48
+ "Task ID": range(1, 26),
49
+ "Description": [f"Task {i}" for i in range(1, 26)]
50
+ })
51
+
52
+ # Act
53
+ chunk_size = 5
54
+ overlap = 2
55
+ results = list(chunk_dataframe_with_context(df, chunk_size=chunk_size, overlap=overlap))
56
+
57
+ # Assert
58
+ # The number of chunks expected = ceil(25 / 5) = 5
59
+ self.assertEqual(len(results), 5)
60
+
61
+ # Check each chunk
62
+ for i, (core_df, extended_df) in enumerate(results):
63
+ # 1) core_df should have at most 5 rows (the chunk_size)
64
+ self.assertTrue(len(core_df) <= chunk_size)
65
+
66
+ # 2) extended_df should be larger or equal to core_df (due to overlap)
67
+ self.assertTrue(len(extended_df) >= len(core_df))
68
+
69
+ # 3) Check that extended_df contains core_df's indices
70
+ # (i.e., the extended rows are a superset of the core rows)
71
+ core_indices = core_df.index.tolist()
72
+ extended_indices = extended_df.index.tolist()
73
+
74
+ for idx in core_indices:
75
+ self.assertIn(idx, extended_indices)
76
+
77
+ # First chunk
78
+ first_core, first_extended = results[0]
79
+ self.assertEqual(first_core.iloc[0]["Task ID"], 1)
80
+ self.assertEqual(first_core.iloc[-1]["Task ID"], 5)
81
+ self.assertEqual(first_extended.iloc[0]["Task ID"], 1)
82
+ self.assertEqual(first_extended.iloc[-1]["Task ID"], 7)
83
+
84
+ # Last chunk
85
+ last_core, last_extended = results[-1]
86
+ self.assertEqual(last_core.iloc[0]["Task ID"], 21)
87
+ self.assertEqual(last_core.iloc[-1]["Task ID"], 25)
88
+ self.assertEqual(last_extended.iloc[0]["Task ID"], 19)
89
+ self.assertEqual(last_extended.iloc[-1]["Task ID"], 25)
src/expert/README.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Expert
2
+
3
+ Criticism from experts
4
+
5
+ - Identify experts that can provide feedback.
6
+ - Ask each expert for feedback.
7
+
src/expert/__init__.py ADDED
File without changes
src/expert/expert_criticism.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.expert.expert_criticism
3
+
4
+ Ask a specific expert about something, and get criticism back or constructive feedback.
5
+ """
6
+ import json
7
+ import time
8
+ from math import ceil
9
+ from typing import Optional
10
+ from dataclasses import dataclass
11
+ from pydantic import BaseModel, Field
12
+ from llama_index.core.llms import ChatMessage, MessageRole
13
+ from llama_index.core.llms.llm import LLM
14
+
15
+ class NegativeFeedbackItem(BaseModel):
16
+ feedback_index: int = Field(description="Incrementing index, such as 1, 2, 3, 4, 5.")
17
+ feedback_title: str = Field(description="Constructive criticism. What is the problem?")
18
+ feedback_verbose: str = Field(description="Elaborate on the criticism. Provide more context and details.")
19
+ feedback_problem_tags: list[str] = Field(description="Short identifiers that describe the problem.")
20
+ feedback_mitigation: str = Field(description="Mitigation plan.")
21
+ feedback_consequence: str = Field(description="Without mitigation what are the consequences.")
22
+ feedback_root_cause: str = Field(description="Possible root cause.")
23
+
24
+ class ExpertConsultation(BaseModel):
25
+ """
26
+ Status after todays meeting with the client.
27
+ """
28
+ negative_feedback_list: list[NegativeFeedbackItem] = Field(description="Your negative feedback.")
29
+ user_primary_actions: list[str] = Field(description="List of actionable steps the user MUST take.")
30
+ user_secondary_actions: list[str] = Field(description="List of actionable steps the user should take.")
31
+ follow_up_consultation: str = Field(description="What to talk about in the next consultation.")
32
+
33
+ EXPERT_CRITICISM_SYSTEM_PROMPT = f"""
34
+ You are acting as a highly experienced:
35
+ PLACEHOLDER_ROLE
36
+
37
+ Your areas of deep knowledge include:
38
+ PLACEHOLDER_KNOWLEDGE
39
+
40
+ You possess the following key skills:
41
+ PLACEHOLDER_SKILLS
42
+
43
+ From your perspective, please analyze the provided document.
44
+
45
+ The client may be off track, provide help to get back on track.
46
+
47
+ The "negative_feedback_list" must contain 3 items.
48
+
49
+ Provide a detailed list of actions that the client must take to address the issues you identify.
50
+
51
+ In the "feedback_mitigation" field, provide a mitigation plan for each issue.
52
+ How can this be improved? Who to consult? What to read? What data to provide?
53
+
54
+ Be brutally direct and provide actionable advice based on your expertise.
55
+
56
+ Be skeptical. There may be deeper unresolved problems and root causes.
57
+
58
+ Focus specifically on areas where your expertise can offer unique insights and actionable advice.
59
+ """
60
+
61
+ @dataclass
62
+ class ExpertCriticism:
63
+ """
64
+ Ask an expert advise about a topic, and get criticism back.
65
+ """
66
+ query: str
67
+ response: dict
68
+ metadata: dict
69
+ feedback_list: list[dict]
70
+ primary_actions: list[str]
71
+ secondary_actions: list[str]
72
+ follow_up: str
73
+
74
+ @classmethod
75
+ def format_system(cls, expert: dict) -> str:
76
+ if not isinstance(expert, dict):
77
+ raise ValueError("Invalid expert.")
78
+
79
+ query = EXPERT_CRITICISM_SYSTEM_PROMPT.strip()
80
+ role = expert.get('title', 'No role specified')
81
+ knowledge = expert.get('knowledge', 'No knowledge specified')
82
+ skills = expert.get('skills', 'No skills specified')
83
+
84
+ query = query.replace("PLACEHOLDER_ROLE", role)
85
+ query = query.replace("PLACEHOLDER_KNOWLEDGE", knowledge)
86
+ query = query.replace("PLACEHOLDER_SKILLS", skills)
87
+ return query
88
+
89
+ @classmethod
90
+ def format_query(cls, document_title: str, document_content: str) -> str:
91
+ if not isinstance(document_title, str):
92
+ raise ValueError("Invalid document_title.")
93
+ if not isinstance(document_content, str):
94
+ raise ValueError("Invalid document_content.")
95
+
96
+ query = f"""
97
+ {document_title}:
98
+ {document_content}
99
+ """
100
+ return query
101
+
102
+ @classmethod
103
+ def execute(cls, llm: LLM, query: str, system_prompt: Optional[str]) -> 'ExpertCriticism':
104
+ """
105
+ Invoke LLM to get advise from the expert.
106
+ """
107
+ if not isinstance(llm, LLM):
108
+ raise ValueError("Invalid LLM instance.")
109
+ if not isinstance(query, str):
110
+ raise ValueError("Invalid query.")
111
+
112
+ chat_message_list = []
113
+ if system_prompt:
114
+ chat_message_list.append(
115
+ ChatMessage(
116
+ role=MessageRole.SYSTEM,
117
+ content=system_prompt,
118
+ )
119
+ )
120
+
121
+ chat_message_user = ChatMessage(
122
+ role=MessageRole.USER,
123
+ content=query,
124
+ )
125
+ chat_message_list.append(chat_message_user)
126
+
127
+ start_time = time.perf_counter()
128
+
129
+ sllm = llm.as_structured_llm(ExpertConsultation)
130
+ chat_response = sllm.chat(chat_message_list)
131
+ json_response = json.loads(chat_response.message.content)
132
+
133
+ end_time = time.perf_counter()
134
+ duration = int(ceil(end_time - start_time))
135
+
136
+ metadata = dict(llm.metadata)
137
+ metadata["llm_classname"] = llm.class_name()
138
+ metadata["duration"] = duration
139
+
140
+ # Cleanup the json response from the LLM model.
141
+ result_feedback_list = []
142
+ for item in json_response['negative_feedback_list']:
143
+ d = {
144
+ 'title': item.get('feedback_title', ''),
145
+ 'verbose': item.get('feedback_verbose', ''),
146
+ 'tags': item.get('feedback_problem_tags', []),
147
+ 'mitigation': item.get('feedback_mitigation', ''),
148
+ 'consequence': item.get('feedback_consequence', ''),
149
+ 'root_cause': item.get('feedback_root_cause', ''),
150
+ }
151
+ result_feedback_list.append(d)
152
+
153
+ result = ExpertCriticism(
154
+ query=query,
155
+ response=json_response,
156
+ metadata=metadata,
157
+ feedback_list=result_feedback_list,
158
+ primary_actions=json_response.get('user_primary_actions', []),
159
+ secondary_actions=json_response.get('user_secondary_actions', []),
160
+ follow_up=json_response.get('follow_up_consultation', '')
161
+ )
162
+ return result
163
+
164
+ def to_dict(self, include_metadata=True, include_query=True) -> dict:
165
+ d = self.response.copy()
166
+ if include_metadata:
167
+ d['metadata'] = self.metadata
168
+ if include_query:
169
+ d['query'] = self.query
170
+ return d
171
+
172
+ def save_raw(self, file_path: str) -> None:
173
+ with open(file_path, 'w') as f:
174
+ f.write(json.dumps(self.to_dict(), indent=2))
175
+
176
+ if __name__ == "__main__":
177
+ from src.llm_factory import get_llm
178
+ import os
179
+
180
+ path1 = os.path.join(os.path.dirname(__file__), 'test_data', 'solarfarm_swot_analysis.md')
181
+ path2 = os.path.join(os.path.dirname(__file__), 'test_data', 'solarfarm_expert_list.json')
182
+
183
+ with open(path1, 'r', encoding='utf-8') as f:
184
+ swot_markdown = f.read()
185
+
186
+ with open(path2, 'r', encoding='utf-8') as f:
187
+ expert_list_json = json.load(f)
188
+
189
+ expert = expert_list_json[5]
190
+ expert.pop('id')
191
+ system_prompt = ExpertCriticism.format_system(expert)
192
+ query = ExpertCriticism.format_query("SWOT Analysis", swot_markdown)
193
+
194
+ llm = get_llm("ollama-llama3.1")
195
+ # llm = get_llm("deepseek-chat")
196
+
197
+ print(f"System: {system_prompt}")
198
+ print(f"\n\nQuery: {query}")
199
+ result = ExpertCriticism.execute(llm, query, system_prompt)
200
+
201
+ print("\n\nResponse:")
202
+ print(json.dumps(result.to_dict(include_query=False), indent=2))
203
+
204
+ print("\n\nFeedback:")
205
+ print(json.dumps(result.feedback_list, indent=2))
src/expert/expert_finder.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.expert.expert_finder
3
+
4
+ Find experts that can take a look at the a document, such as a 'SWOT analysis' and provide feedback.
5
+
6
+ IDEA: Specify a number of experts to be obtained. Currently it's hardcoded 8.
7
+ When it's 4 or less, then there is no need to make a second call to the LLM model.
8
+ When it's 9 or more, then make multiple calls to the LLM model to get more experts.
9
+ """
10
+ import json
11
+ import time
12
+ import logging
13
+ from math import ceil
14
+ from uuid import uuid4
15
+ from typing import List, Optional, Any
16
+ from dataclasses import dataclass
17
+ from pydantic import BaseModel, Field
18
+ from llama_index.core.llms.llm import LLM
19
+ from llama_index.core.llms import ChatMessage, MessageRole
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class Expert(BaseModel):
24
+ expert_title: str = Field(description="Job title of the expert.")
25
+ expert_knowledge: str = Field(description="Industry Knowledge/Specialization, specific industries or subfields where they have focused their career, such as: tech industry for an IT consultant, healthcare sector for a medical expert. **Must be a brief comma separated list**.")
26
+ expert_why: str = Field(description="Why can this expert be of help. Area of expertise.")
27
+ expert_what: str = Field(description="Describe what area of this document the role should advise about.")
28
+ expert_relevant_skills: str = Field(description="Skills that are relevant to the document.")
29
+ expert_search_query: str = Field(description="What query to use when searching for this expert.")
30
+
31
+ class ExpertDetails(BaseModel):
32
+ experts: list[Expert] = Field(description="List of experts.")
33
+
34
+ EXPERT_FINDER_SYSTEM_PROMPT = """
35
+ Professionals who can offer specialized perspectives and recommendations based on the document.
36
+
37
+ Ensure that each expert role directly aligns with specific sections or themes within the document.
38
+ This could involve linking particular strengths, weaknesses, opportunities, threats, extra sections, to the expertise required.
39
+
40
+ Diversity in the types of experts suggested by considering interdisciplinary insights that might not be
41
+ immediately obvious but could offer unique perspectives on the document.
42
+
43
+ Account for geographical and contextual relevance, variations in terminology or regional differences that may affect the search outcome.
44
+
45
+ The "expert_search_query" field is a human readable text for searching in Google/DuckDuckGo/LinkedIn.
46
+
47
+ Find exactly 4 experts.
48
+ """
49
+
50
+ @dataclass
51
+ class ExpertFinder:
52
+ """
53
+ Find experts that can advise about the particular domain.
54
+ """
55
+ system_prompt: Optional[str]
56
+ user_prompt: str
57
+ response: dict
58
+ metadata: dict
59
+ expert_list: list[dict]
60
+
61
+ @classmethod
62
+ def execute(cls, llm: LLM, user_prompt: str, **kwargs: Any) -> 'ExpertFinder':
63
+ """
64
+ Invoke LLM to find the best suited experts that can advise about attached file.
65
+ """
66
+ if not isinstance(llm, LLM):
67
+ raise ValueError("Invalid LLM instance.")
68
+ if not isinstance(user_prompt, str):
69
+ raise ValueError("Invalid query.")
70
+
71
+ default_args = {
72
+ 'system_prompt': EXPERT_FINDER_SYSTEM_PROMPT.strip()
73
+ }
74
+ default_args.update(kwargs)
75
+
76
+ system_prompt = default_args.get('system_prompt')
77
+ logger.debug(f"System Prompt:\n{system_prompt}")
78
+ if system_prompt and not isinstance(system_prompt, str):
79
+ raise ValueError("Invalid system prompt.")
80
+
81
+ chat_message_list1 = []
82
+ if system_prompt:
83
+ chat_message_list1.append(
84
+ ChatMessage(
85
+ role=MessageRole.SYSTEM,
86
+ content=system_prompt,
87
+ )
88
+ )
89
+
90
+ logger.debug(f"User Prompt:\n{user_prompt}")
91
+ chat_message_user1 = ChatMessage(
92
+ role=MessageRole.USER,
93
+ content=user_prompt,
94
+ )
95
+ chat_message_list1.append(chat_message_user1)
96
+
97
+ sllm = llm.as_structured_llm(ExpertDetails)
98
+
99
+ logger.debug("Starting LLM chat interaction 1.")
100
+ start_time = time.perf_counter()
101
+ chat_response1 = sllm.chat(chat_message_list1)
102
+ end_time = time.perf_counter()
103
+ duration1 = int(ceil(end_time - start_time))
104
+ response_byte_count1 = len(chat_response1.message.content.encode('utf-8'))
105
+ logger.info(f"LLM chat interaction completed in {duration1} seconds. Response byte count: {response_byte_count1}")
106
+
107
+ # Do a follow up question, for obtaining more experts.
108
+ chat_message_assistant2 = ChatMessage(
109
+ role=MessageRole.ASSISTANT,
110
+ content=chat_response1.message.content,
111
+ )
112
+ chat_message_user2 = ChatMessage(
113
+ role=MessageRole.USER,
114
+ content="4 more please",
115
+ )
116
+ chat_message_list2 = chat_message_list1.copy()
117
+ chat_message_list2.append(chat_message_assistant2)
118
+ chat_message_list2.append(chat_message_user2)
119
+
120
+ logger.debug("Starting LLM chat interaction 2.")
121
+ start_time = time.perf_counter()
122
+ chat_response2 = sllm.chat(chat_message_list2)
123
+ end_time = time.perf_counter()
124
+ duration2 = int(ceil(end_time - start_time))
125
+ response_byte_count2 = len(chat_response2.message.content.encode('utf-8'))
126
+ logger.info(f"LLM chat interaction completed in {duration2} seconds. Response byte count: {response_byte_count2}")
127
+
128
+ metadata = dict(llm.metadata)
129
+ metadata["llm_classname"] = llm.class_name()
130
+ metadata["duration1"] = duration1
131
+ metadata["duration2"] = duration2
132
+ metadata["response_byte_count1"] = response_byte_count1
133
+ metadata["response_byte_count2"] = response_byte_count2
134
+
135
+ json_response1 = json.loads(chat_response1.message.content)
136
+ json_response2 = json.loads(chat_response2.message.content)
137
+
138
+ json_response_merged = {}
139
+ experts1 = json_response1.get('experts', [])
140
+ experts2 = json_response2.get('experts', [])
141
+ json_response_merged['experts'] = experts1 + experts2
142
+
143
+ # Cleanup the json response from the LLM model, extract the experts.
144
+ expert_list = []
145
+ for expert in json_response_merged['experts']:
146
+ uuid = str(uuid4())
147
+ expert_dict = {
148
+ "id": uuid,
149
+ "title": expert['expert_title'],
150
+ "knowledge": expert['expert_knowledge'],
151
+ "why": expert['expert_why'],
152
+ "what": expert['expert_what'],
153
+ "skills": expert['expert_relevant_skills'],
154
+ "search_query": expert['expert_search_query'],
155
+ }
156
+ expert_list.append(expert_dict)
157
+
158
+ logger.info(f"Found {len(expert_list)} experts.")
159
+
160
+ result = ExpertFinder(
161
+ system_prompt=system_prompt,
162
+ user_prompt=user_prompt,
163
+ response=json_response_merged,
164
+ metadata=metadata,
165
+ expert_list=expert_list,
166
+ )
167
+ logger.debug("CreateProjectPlan instance created successfully.")
168
+ return result
169
+
170
+ def to_dict(self, include_metadata=True, include_system_prompt=True, include_user_prompt=True) -> dict:
171
+ d = self.response.copy()
172
+ if include_metadata:
173
+ d['metadata'] = self.metadata
174
+ if include_system_prompt:
175
+ d['system_prompt'] = self.system_prompt
176
+ if include_user_prompt:
177
+ d['user_prompt'] = self.user_prompt
178
+ return d
179
+
180
+ def save_raw(self, file_path: str) -> None:
181
+ with open(file_path, 'w') as f:
182
+ f.write(json.dumps(self.to_dict(), indent=2))
183
+
184
+ def save_cleanedup(self, file_path: str) -> None:
185
+ with open(file_path, 'w') as f:
186
+ f.write(json.dumps(self.expert_list, indent=2))
187
+
188
+
189
+ if __name__ == "__main__":
190
+ import logging
191
+ from src.llm_factory import get_llm
192
+ import os
193
+
194
+ logging.basicConfig(
195
+ level=logging.DEBUG,
196
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
197
+ handlers=[
198
+ logging.StreamHandler()
199
+ ]
200
+ )
201
+
202
+ path = os.path.join(os.path.dirname(__file__), 'test_data', 'solarfarm_swot_analysis.md')
203
+ with open(path, 'r', encoding='utf-8') as f:
204
+ swot_markdown = f.read()
205
+
206
+ query = f"SWOT Analysis:\n{swot_markdown}"
207
+
208
+ llm = get_llm("ollama-llama3.1")
209
+ # llm = get_llm("deepseek-chat", max_tokens=8192)
210
+
211
+ print(f"Query: {query}")
212
+ result = ExpertFinder.execute(llm, query)
213
+
214
+ print("\n\nResponse:")
215
+ print(json.dumps(result.to_dict(include_system_prompt=False, include_user_prompt=False), indent=2))
216
+
217
+ print("\n\nExperts:")
218
+ print(json.dumps(result.expert_list, indent=2))
src/expert/expert_orchestrator.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.expert.expert_orchestrator
3
+ """
4
+ import logging
5
+ from llama_index.core.llms.llm import LLM
6
+ from src.expert.expert_finder import ExpertFinder
7
+ from src.expert.expert_criticism import ExpertCriticism
8
+ from src.expert.markdown_with_criticism_from_experts import markdown_rows_with_info_about_one_expert, markdown_rows_with_criticism_from_one_expert
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class ExpertOrchestrator:
13
+ def __init__(self):
14
+ self.phase1_post_callback = None
15
+ self.phase2_post_callback = None
16
+ self.expert_finder: ExpertFinder = None
17
+ self.expert_criticism_list: list[ExpertCriticism] = []
18
+ self.max_expert_count = 2
19
+
20
+ def execute(self, llm: LLM, query: str) -> None:
21
+ logger.info("Finding experts that can provide criticism...")
22
+
23
+ self.expert_finder = ExpertFinder.execute(llm, query)
24
+ if self.phase1_post_callback:
25
+ self.phase1_post_callback(self.expert_finder)
26
+
27
+ expert_finder = self.expert_finder
28
+ all_expert_list = expert_finder.expert_list
29
+ all_expert_count = len(all_expert_list)
30
+
31
+ expert_list_truncated = all_expert_list[:self.max_expert_count]
32
+ expert_list_truncated_count = len(expert_list_truncated)
33
+
34
+ if all_expert_count != expert_list_truncated_count:
35
+ logger.info(f"Truncated expert list from {all_expert_count} to {expert_list_truncated_count} experts.")
36
+
37
+ logger.info(f"Asking {expert_list_truncated_count} experts for criticism...")
38
+
39
+ for expert_index, expert_dict in enumerate(expert_list_truncated):
40
+ expert_copy = expert_dict.copy()
41
+ expert_copy.pop('id')
42
+ expert_title = expert_copy.get('title', 'Missing title')
43
+ logger.info(f"Getting criticism from expert {expert_index + 1} of {expert_list_truncated_count}. expert_title: {expert_title}")
44
+ system_prompt = ExpertCriticism.format_system(expert_dict)
45
+ expert_criticism = ExpertCriticism.execute(llm, query, system_prompt)
46
+ if self.phase2_post_callback:
47
+ self.phase2_post_callback(expert_criticism, expert_index)
48
+ self.expert_criticism_list.append(expert_criticism)
49
+
50
+ logger.info(f"Finished collecting criticism from {expert_list_truncated_count} experts.")
51
+
52
+ def to_markdown(self) -> str:
53
+ rows = []
54
+ rows.append("# Project Expert Review & Recommendations\n")
55
+ rows.append("## A Compilation of Professional Feedback for Project Planning and Execution\n\n")
56
+
57
+ number_of_experts_with_criticism = len(self.expert_criticism_list)
58
+ for expert_index, expert_criticism in enumerate(self.expert_criticism_list):
59
+ section_index = expert_index + 1
60
+ if expert_index > 0:
61
+ rows.append("\n---\n")
62
+ expert_details = self.expert_finder.expert_list[expert_index]
63
+ rows.extend(markdown_rows_with_info_about_one_expert(section_index, expert_details))
64
+ rows.extend(markdown_rows_with_criticism_from_one_expert(section_index, expert_criticism.to_dict()))
65
+
66
+ if number_of_experts_with_criticism != len(self.expert_finder.expert_list):
67
+ rows.append("\n---\n")
68
+ rows.append("# The following experts did not provide feedback:")
69
+ for expert_index, expert_details in enumerate(self.expert_finder.expert_list):
70
+ if expert_index < number_of_experts_with_criticism:
71
+ continue
72
+ section_index = expert_index + 1
73
+ rows.append("")
74
+ rows.extend(markdown_rows_with_info_about_one_expert(section_index, expert_details))
75
+
76
+ return "\n".join(rows)
77
+
78
+ if __name__ == "__main__":
79
+ import logging
80
+ from src.llm_factory import get_llm
81
+ from src.plan.find_plan_prompt import find_plan_prompt
82
+ import json
83
+
84
+ logging.basicConfig(
85
+ level=logging.INFO,
86
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
87
+ handlers=[
88
+ logging.StreamHandler()
89
+ ]
90
+ )
91
+
92
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
93
+
94
+ llm = get_llm("ollama-llama3.1")
95
+ # llm = get_llm("openrouter-paid-gemini-2.0-flash-001")
96
+ # llm = get_llm("deepseek-chat")
97
+
98
+ def phase1_post_callback(expert_finder: ExpertFinder) -> None:
99
+ count = len(expert_finder.expert_list)
100
+ d = expert_finder.to_dict(include_system_prompt=False, include_user_prompt=False)
101
+ pretty = json.dumps(d, indent=2)
102
+ print(f"Found {count} expert:\n{pretty}")
103
+
104
+ def phase2_post_callback(expert_criticism: ExpertCriticism, expert_index: int) -> None:
105
+ d = expert_criticism.to_dict(include_query=False)
106
+ pretty = json.dumps(d, indent=2)
107
+ print(f"Expert {expert_index + 1} criticism:\n{pretty}")
108
+
109
+ orchestrator = ExpertOrchestrator()
110
+ orchestrator.phase1_post_callback = phase1_post_callback
111
+ orchestrator.phase2_post_callback = phase2_post_callback
112
+ orchestrator.execute(llm, plan_prompt)
113
+ print("\n\nMarkdown:")
114
+ print(orchestrator.to_markdown())
src/expert/markdown_with_criticism_from_experts.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def markdown_rows_with_info_about_one_expert(section_index: int, expert_detail_json: dict) -> list[str]:
2
+ rows = []
3
+
4
+ expert_title = expert_detail_json.get('title', 'Missing title')
5
+ rows.append(f"# {section_index} Expert: {expert_title}")
6
+
7
+ expert_knowledge = expert_detail_json.get('knowledge', 'Missing knowledge')
8
+ rows.append(f"\n**Knowledge**: {expert_knowledge}")
9
+
10
+ expert_why = expert_detail_json.get('why', 'Missing why')
11
+ rows.append(f"\n**Why**: {expert_why}")
12
+
13
+ expert_what = expert_detail_json.get('what', 'Missing what')
14
+ rows.append(f"\n**What**: {expert_what}")
15
+
16
+ expert_skills = expert_detail_json.get('skills', 'Missing skills')
17
+ rows.append(f"\n**Skills**: {expert_skills}")
18
+
19
+ expert_search_query = expert_detail_json.get('search_query', 'Missing search_query')
20
+ rows.append(f"\n**Search**: {expert_search_query}")
21
+
22
+ return rows
23
+
24
+ def markdown_rows_with_criticism_from_one_expert(section_index: int, expert_criticism_json: dict) -> list[str]:
25
+ rows = []
26
+
27
+ rows.append("")
28
+ user_primary_actions = expert_criticism_json.get('user_primary_actions', None)
29
+ rows.append(f"## {section_index}.1 Primary Actions\n")
30
+ if isinstance(user_primary_actions, list) and len(user_primary_actions) > 0:
31
+ for action in user_primary_actions:
32
+ rows.append(f"- {action}")
33
+ else:
34
+ rows.append("Empty")
35
+
36
+ user_secondary_actions = expert_criticism_json.get('user_secondary_actions', None)
37
+ rows.append(f"\n## {section_index}.2 Secondary Actions\n")
38
+ if isinstance(user_secondary_actions, list) and len(user_secondary_actions) > 0:
39
+ for action in user_secondary_actions:
40
+ rows.append(f"- {action}")
41
+ else:
42
+ rows.append("Empty")
43
+
44
+ follow_up_consultation = expert_criticism_json.get('follow_up_consultation', None)
45
+ rows.append(f"\n## {section_index}.3 Follow Up Consultation\n")
46
+ if follow_up_consultation:
47
+ rows.append(follow_up_consultation)
48
+ else:
49
+ rows.append("Empty")
50
+
51
+ start_subsection_index = 4
52
+ negative_feedback_list = expert_criticism_json.get('negative_feedback_list', [])
53
+ for feedback_index, feedback_item in enumerate(negative_feedback_list):
54
+ rows.append("")
55
+
56
+ subsection_index = start_subsection_index + feedback_index
57
+ prefix_a = f"{section_index}.{subsection_index}.A"
58
+ prefix_b = f"{section_index}.{subsection_index}.B"
59
+ prefix_c = f"{section_index}.{subsection_index}.C"
60
+ prefix_d = f"{section_index}.{subsection_index}.D"
61
+ prefix_e = f"{section_index}.{subsection_index}.E"
62
+
63
+ title = feedback_item.get('feedback_title', 'Missing feedback_title')
64
+ feedback_verbose = feedback_item.get('feedback_verbose', 'Missing feedback_verbose')
65
+ rows.append(f"## {prefix_a} Issue - {title}\n")
66
+ rows.append(feedback_verbose)
67
+
68
+ problem_tag_list = feedback_item.get('feedback_problem_tags', None)
69
+ rows.append(f"\n### {prefix_b} Tags\n")
70
+ if isinstance(problem_tag_list, list) and len(problem_tag_list) > 0:
71
+ for tag in problem_tag_list:
72
+ rows.append(f"- {tag}")
73
+ else:
74
+ rows.append("Empty")
75
+
76
+ feedback_mitigation = feedback_item.get('feedback_mitigation', None)
77
+ rows.append(f"\n### {prefix_c} Mitigation\n")
78
+ if feedback_mitigation:
79
+ rows.append(feedback_mitigation)
80
+ else:
81
+ rows.append("Empty")
82
+
83
+ feedback_consequence = feedback_item.get('feedback_consequence', None)
84
+ rows.append(f"\n### {prefix_d} Consequence\n")
85
+ if feedback_consequence:
86
+ rows.append(feedback_consequence)
87
+ else:
88
+ rows.append("Empty")
89
+
90
+ feedback_root_cause = feedback_item.get('feedback_root_cause', None)
91
+ rows.append(f"\n### {prefix_e} Root Cause\n")
92
+ if feedback_root_cause:
93
+ rows.append(feedback_root_cause)
94
+ else:
95
+ rows.append("Empty")
96
+
97
+ return rows
src/expert/pre_project_assessment.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.expert.pre_project_assessment
3
+
4
+ Two experts analyze a project plan and provide feedback.
5
+
6
+ Analysis: Experts assess the plan.
7
+
8
+ Feedback Generation: Experts create actionable feedback.
9
+
10
+ Summary & Recommendation: A summary and a "Go/No Go" recommendation is created based on the experts input.
11
+
12
+ IDEA: Cloud providers often timeouts.
13
+ If a cloud provider is down, it may take hours to get back up. In that case switch to another cloud provider.
14
+ It would be useful to cycle through multiple cloud providers to get the best results.
15
+ Keeping track of health of each cloud providers would be useful, up/down status, response time, etc.
16
+
17
+ IDEA: Pool of multiple system prompts. Some of the system prompts yields better results than others.
18
+ Run 3 invocation of the LLM, using different system prompts. Select the best result. Or extract the best parts from each result.
19
+ Example of where it makes sense to have, a pool of multiple system prompts:
20
+ For a simple programming task, one system prompt may focus on the irrelevant things, such as:
21
+ - Procure the Python library 'Pygame' to handle graphics and game logic.
22
+ It makes no sense to procure a Python library for a programming task. It's a simple pip install command.
23
+ - Ensure compatibility with your system architecture (32-bit or 64-bit).
24
+ The year is 2025, I haven't dealt with 32 bit issues for the last 20 years.
25
+ """
26
+ import json
27
+ import time
28
+ from datetime import datetime
29
+ import logging
30
+ from math import ceil
31
+ from uuid import uuid4
32
+ from typing import List, Optional, Any
33
+ from dataclasses import dataclass
34
+ from pydantic import BaseModel, Field
35
+ from llama_index.core.llms.llm import LLM
36
+ from llama_index.core.llms import ChatMessage, MessageRole
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ class FeedbackItem(BaseModel):
41
+ feedback_index: int = Field(description="The index of the feedback item.")
42
+ feedback_title: str = Field(description="What is the feedback about?")
43
+ feedback_description: str = Field(description="Describe the feedback.")
44
+
45
+ class Expert(BaseModel):
46
+ expert_title: str = Field(description="Job title of the expert.")
47
+ expert_full_name: str = Field(description="First name and last name of the expert.")
48
+ feedback_item_list: List[FeedbackItem] = Field(description="List of feedback items.")
49
+
50
+ class ExpertDetails(BaseModel):
51
+ expert1: Expert = Field(description="Perspective from expert 1.")
52
+ expert2: Expert = Field(description="Perspective from expert 2.")
53
+ combined_summary: str = Field(description="Summary of the feedback from both experts.")
54
+ go_no_go_recommendation: str = Field(description="A 'Go' or 'No Go' recommendation, with an explanation.")
55
+
56
+ # Prompt made with o1mini
57
+ EXPERT_BROAD_SYSTEM_PROMPT_1 = """
58
+ You are a team of 2 experts providing a critical review of a project with a vague description. Depending on the project type, select appropriate expert roles.
59
+
60
+ **Requirements:**
61
+
62
+ 1. **Feedback Items:**
63
+ - Each expert must provide exactly **4 feedback items**.
64
+ - Each feedback item must start with **"You must do this:"** and include **3-4 specific reasons or actions**.
65
+ - The **"feedback_title"** should capture the essence of the feedback in **around 7 words**.
66
+ - Use **consistent and professional language** throughout all feedback items.
67
+ - **Avoid redundancy** between experts; ensure each expert addresses distinct aspects of the project.
68
+ - Each "feedback_description" should provide clear, step-by-step actions that are specific and measurable.
69
+ - Avoid vague statements; ensure that each action is actionable and can be directly implemented.
70
+
71
+ **Focus Areas:**
72
+ - **Expert 1:** Project management, technical feasibility, financial modeling, and stakeholder engagement.
73
+ - **Expert 2:** Environmental impact, regulatory compliance, community engagement, and risk management.
74
+
75
+ 2. **Combined Summary and Recommendation:**
76
+ - **"combined_summary":** Summarize the **3 most critical reasons** why the project cannot start tomorrow.
77
+ - **"go_no_go_recommendation":** Provide a clear **"Go"** or **"No Go"** recommendation with a brief explanation.
78
+ """
79
+
80
+ # Prompt made with gemini
81
+ EXPERT_BROAD_SYSTEM_PROMPT_2 = """
82
+ Pretend that you are a team of 2 experts providing a critical review of a project with a vague description. You must provide specific, actionable recommendations, including why the project cannot begin tomorrow. Each feedback item must be a specific reason why the project cannot begin tomorrow. Each feedback item must start with 'You must do this:'. Each feedback item should then be broken down into 3-4 specific reasons.
83
+
84
+ The "feedback_title" must capture the essence of the feedback, use around 7 words.
85
+
86
+ The "feedback_item_list" must contain 4 items per expert. The response must have consistent language throughout all feedback items.
87
+
88
+ The "expert_full_name" is a fictional name, that might be plausible for the expert.
89
+
90
+ You must provide a "Go" or "No Go" recommendation. You must also provide the reasons for that recommendation.
91
+
92
+ The "combined_summary" must include the 3 most important and critical reasons why the project cannot start tomorrow, and the actions you recommend to address these reasons.
93
+
94
+ The goal of the experts is to assess the readiness and feasibility of the project, and to identify any risks that would make a 'start tomorrow' plan, unfeasible.
95
+ """
96
+
97
+ # Prompt made with gemini, by combining the two previous prompts
98
+ EXPERT_BROAD_SYSTEM_PROMPT_3 = """
99
+ You are a team of 2 experts providing a critical review of a task with a vague description. The task is short-term and requires immediate attention.
100
+
101
+ Your goal is to assess how to complete the task safely and quickly, providing very specific and actionable steps. Select appropriate expert roles.
102
+
103
+ **Requirements:**
104
+
105
+ 1. **Expert Roles:**
106
+ - **Expert 1:** Focus on how to complete the task as quickly and *efficiently* as possible, with very specific, actionable steps.
107
+ - **Expert 2:** Focus on the safety aspects of the task, with very specific, actionable steps to mitigate safety concerns.
108
+ - Each expert must have an appropriate title and a fictional full name, relevant to the chosen roles for the task.
109
+
110
+ 2. **Feedback Items:**
111
+ - Each expert *MUST* provide exactly **4 feedback items**.
112
+ - Each feedback item must start with **"To execute the task, you must:"** followed by **3-4 *extremely specific, concrete, actionable steps*.** Avoid vague or high-level steps. Each step should include specific details such as measurements, timings, equipment, or precise actions required. For example instead of "handle hot water carefully" use "Wear oven mitts when handling hot water and pour slowly and steadily".
113
+ - The **"feedback_title"** should capture the essence of the feedback in around **7 words**, focusing on the immediate actions to be taken. The title should imply very specific actions.
114
+ - The feedback items MUST NOT be too high level, and MUST be very specific.
115
+
116
+ 3. **Combined Summary and Recommendation:**
117
+ - **"combined_summary":** Summarize the **3 most critical actions**, using specific examples from the feedback items, needed immediately to enable the task to begin. The summary must reference the `feedback_index` from the experts for each of these three critical actions. Explain *why* those 3 actions are the most critical actions needed.
118
+ - **"go_no_go_recommendation":**
119
+ Provide a clear **"Execute Immediately"** or **"Do Not Execute"** recommendation.
120
+ """
121
+
122
+ # Prompt made with deepseek, by combining the 3 previous prompts
123
+ EXPERT_BROAD_SYSTEM_PROMPT_4 = """
124
+ You are a team of 2 experts providing a critical review of a project with a vague description. The project can be short-term, medium-term, or long-term. Your goal is to assess how to complete the project safely, efficiently, and effectively, providing very specific and actionable steps. Select appropriate expert roles based on the project type.
125
+
126
+ **Requirements:**
127
+
128
+ 1. **Expert Roles:**
129
+ - **Expert 1:** Focus on how to complete the project as quickly and *efficiently* as possible, with very specific, actionable steps. This includes project management, technical feasibility, resource allocation, and timeline optimization.
130
+ - **Expert 2:** Focus on the safety, compliance, and risk mitigation aspects of the project, with very specific, actionable steps to address potential hazards, regulatory requirements, and environmental or community impacts.
131
+ - Each expert must have an appropriate title and a fictional full name, relevant to the chosen roles for the task.
132
+
133
+ 2. **Feedback Items:**
134
+ - Each expert *MUST* provide exactly **4 feedback items**.
135
+ - Each feedback item must start with **"To execute the project, you must:"** followed by **3-4 *extremely specific, concrete, actionable steps*.** Avoid vague or high-level steps. Each step should include specific details such as measurements, timings, equipment, or precise actions required. For example, instead of "handle hot water carefully," use "Wear oven mitts when handling hot water and pour slowly and steadily."
136
+ - The **"feedback_title"** should capture the essence of the feedback in around **7 words**, focusing on immediate actions to be taken. The title should imply very specific actions (e.g., "Assemble Team by [specific date]").
137
+ - The feedback items MUST NOT be too high-level and MUST be very specific.
138
+
139
+ 3. **Combined Summary and Recommendation:**
140
+ - **"combined_summary":** Summarize the **3 most critical actions**, using specific examples from the feedback items, needed immediately to enable the project to begin. The summary must reference the `feedback_index` from the experts for each of these three critical actions. Explain *why* those 3 actions are the most critical actions needed.
141
+ - **"go_no_go_recommendation":** Provide a clear **"Execute Immediately"**, **"Proceed with Caution"**, or **"Do Not Execute"** recommendation, depending on the project's feasibility, risks, and readiness. Include a brief explanation for the recommendation, addressing potential risks and mitigation strategies.
142
+
143
+ **Focus Areas for Experts:**
144
+ - **Expert 1 (Efficiency and Execution):** Prioritize speed, resource optimization, and technical feasibility. Address logistical challenges, stakeholder coordination, and timeline management.
145
+ - **Expert 2 (Safety and Compliance):** Prioritize risk mitigation, regulatory compliance, and environmental or community impacts. Address safety protocols, hazard prevention, and legal or ethical considerations.
146
+
147
+ **Adaptability:**
148
+ - For **short-term projects**, emphasize immediate actions, rapid resource allocation, and quick risk assessments.
149
+ - For **medium-term projects**, balance efficiency with thorough planning, including phased execution and contingency planning.
150
+ - For **long-term projects**, focus on sustainability, regulatory approvals, and long-term risk management.
151
+
152
+ **Language and Tone:**
153
+ - Use **consistent and professional language** throughout all feedback items.
154
+ - Avoid redundancy between experts; ensure each expert addresses distinct aspects of the project.
155
+ - Ensure all steps are **actionable, measurable, and specific**, avoiding vague or generic advice.
156
+ - Use **action-oriented titles** that imply immediate, specific actions (e.g., "Complete Risk Assessment by [specific date]").
157
+
158
+ **Additional Guidelines:**
159
+ - Include **specific deadlines** (e.g., "by year-month-day") in feedback items to emphasize urgency.
160
+ - Provide **quantifiable details** (e.g., "500 PPE kits," "10m x 10m command center") to ensure clarity and measurability.
161
+ - Highlight **potential risks** and **mitigation strategies** in the combined summary and recommendation.
162
+ - Ensure feedback items are **distinct and non-overlapping** between experts, with clear separation of responsibilities.
163
+ """
164
+
165
+ # Prompt made with gemini, by combining the 4 previous prompts
166
+ EXPERT_BROAD_SYSTEM_PROMPT_5 = """
167
+ You are a team of 2 experts providing a critical, actionable review of a project given its vague description. Your goal is to rapidly assess the project's feasibility, identify key risks, and provide a clear path forward. The project can be short-term, medium-term, or long-term.
168
+
169
+ **Overall Requirements:**
170
+
171
+ 1. **Action-Oriented:** Focus on providing **immediate, concrete steps** that the project team can take to move forward (or decide not to move forward). Avoid analysis or commentary about why those steps need to be done - just list what to do.
172
+ 2. **Feasibility and Risk-Based:** Analyze the project for feasibility given the vague description, and highlight any safety, logistical, or ethical concerns.
173
+ 3. **Clear Recommendation:** Provide a definitive and clear recommendation on whether the project should proceed *now*, and why.
174
+
175
+ **Expert Roles:**
176
+
177
+ - **Expert 1 (Project Execution & Logistics):** Focus on the **practical steps** required to execute the project efficiently. This includes defining resources, timelines, tasks, and initial goals. Prioritize speed, and assume that no prior work has been done on the project.
178
+ - **Expert 2 (Safety, Compliance & Risk):** Focus on **identifying and mitigating risks** related to the project. This includes health, safety, legal and ethical issues. Assume that the team will not consider these issues unless prompted.
179
+
180
+ - Each expert MUST have an appropriate title and a fictional full name relevant to their expertise.
181
+
182
+ **Feedback Items (for each expert):**
183
+
184
+ - Each expert MUST provide exactly **4 feedback items**.
185
+ - Each feedback item must start with **"To initiate this project, you must:"**, followed by **3-4 *extremely specific, concrete, actionable steps*.** Each step should include specific details such as measurements, timings, equipment, personnel requirements or precise actions. *DO NOT* use vague, high-level or generalized statements. The goal is to provide a checklist that can be immediately executed. Use action-oriented language. Use quantifiable details (e.g., "10 meters of rope", "5 sterile collection tubes"). For example, instead of: "procure appropriate gear", instead use: "Procure 10 sets of specialized radiation-resistant suits, including lead-lined inner layers, gloves, and boots rated for 100mSv exposure within 48 hours."
186
+ - The **"feedback_title"** should capture the essence of the feedback in **around 7 words** and should imply very specific actions (e.g., "Procure Safety Gear", "Map the Area"). It should not be a general statement, and it should use an active verb. The 'feedback_title' must NOT include the text 'by Date', as this is unnecessary.
187
+ - The feedback items MUST NOT be too high level, and MUST be very specific. The aim should be to provide a checklist that can be rapidly assessed by any project team. The items must be directly executable as a checklist. When describing quantities, always use phrases such as "at least X" or "no more than X" unless the exact amount is known. All timeframes MUST include a specific date AND time, and the time must be expressed as a 24 hour clock using HH:MM format. If a specific action is likely to be difficult to achieve in the timeframe, the response MUST include an alternative action to mitigate this risk. *Use a bulleted list for all steps, do not include numbered lists.*
188
+
189
+ **Combined Summary and Recommendation:**
190
+
191
+ - **"combined_summary":** Summarize the **3 most critical, immediate actions**, selected from the feedback items across *both* experts, referencing each feedback item using the expert name and `feedback_index`. Explain *why* these actions are the most immediately essential, and how they mitigate the most important risks.
192
+ - **"go_no_go_recommendation":** Provide a clear recommendation of **"Execute Immediately"**, **"Proceed with Caution"**, or **"Do Not Execute"**. Your recommendation must be based on a balanced assessment of the project's potential risks and feasibility, given the limitations of the provided description and the immediate actions outlined by the experts. Do not default to a single recommendation. The response must show that it has actively considered all three options, and show why it is recommending one option over the other two. If you recommend "Proceed with Caution", include the specific actions required for caution. If you recommend "Do Not Execute" be clear about why that's the best option given the risks. The recommendation should be a reflection of the overall safety and operational concerns, given the described project. Provide a brief, *concrete* explanation supporting this recommendation, highlighting the primary risks or critical actions that have influenced the decision.
193
+
194
+ **Additional Guidelines:**
195
+
196
+ - Use **consistent, professional, action-oriented language** throughout.
197
+ - **Avoid redundancy** between experts; ensure each expert addresses distinct aspects.
198
+ - Include **specific deadlines** (e.g., "by year-month-day") or timings to emphasize urgency.
199
+ - Include **quantifiable details** (e.g., "10 meters of rope", "5 sterile collection tubes") to ensure clarity and measurability.
200
+ - The tone should be that of a professional, who has seen many projects, and therefore immediately recognizes key issues that must be resolved for this project to proceed.
201
+ - Be aware that some timelines may be impossible. If a timeline is unrealistic, the response should provide an alternative approach to obtain those results, rather than just accepting the unrealistic timeframe as given. Do not propose that a government permit can be obtained in a single day.
202
+ """
203
+
204
+ # Prompt made with gemini, by combining the 5 previous prompts
205
+ EXPERT_BROAD_SYSTEM_PROMPT_6 = """
206
+ You are a team of 2 experts providing a critical, actionable review of a project given its vague description. Your goal is to rapidly assess the project's feasibility, identify key risks, and provide a clear path forward. The project can be short-term, medium-term, or long-term. The year is CURRENT_YEAR_PLACEHOLDER.
207
+
208
+ **Overall Requirements:**
209
+
210
+ 1. **Action-Oriented:** Focus on providing **immediate, concrete steps** that the project team can take to move forward (or decide not to move forward). Avoid analysis or commentary about why those steps need to be done - just list what to do.
211
+ 2. **Feasibility and Risk-Based:** Analyze the project for feasibility given the vague description, and highlight any safety, logistical, or ethical concerns.
212
+ 3. **Clear Recommendation:** Provide a definitive and clear recommendation on whether the project should proceed *now*, and why.
213
+
214
+ **Expert Roles:**
215
+
216
+ - **Expert 1 (Project Execution & Logistics):** Focus on the **practical steps** required to execute the project efficiently. This includes defining resources, timelines, tasks, and initial goals. Prioritize speed, and assume that no prior work has been done on the project. The feedback *MUST* be specific and derived *ONLY* from the vague description provided. Do *NOT* use generic steps or project management steps. The steps should provide specific details about code, mathematical, and logical details, *if directly implied by the project description*. You MUST explain *why* a specific action is needed.
217
+ - **Expert 2 (Safety, Compliance & Risk):** Focus on **identifying and mitigating risks** related to the project. This includes health, safety, legal and ethical issues, as well as technical risks within the project. Assume that the team will not consider these issues unless prompted. The feedback *MUST* be specific and derived *ONLY* from the vague description provided. Do *NOT* use generic safety steps or general safety advice. If the task is about software, focus on the specific details of *how* to mitigate a risk, and avoid describing the risk itself. You MUST explain *why* a specific action is needed.
218
+
219
+ - Each expert MUST have an appropriate title and a fictional full name relevant to their expertise.
220
+
221
+ **Feedback Items (for each expert):**
222
+
223
+ - Each expert MUST provide exactly **4 feedback items**.
224
+ - Each feedback item must start with **"To initiate this project, you must:"**, followed by **3-4 *extremely specific, concrete, actionable steps*.** Each step should include specific details such as measurements, timings, equipment, personnel requirements or precise actions. *DO NOT* use vague, high-level or generalized statements. The goal is to provide a checklist that can be immediately executed. Use action-oriented language. Use quantifiable details (e.g., "10 meters of rope", "5 sterile collection tubes"). For example, instead of: "procure appropriate gear", instead use: "Procure 10 sets of specialized radiation-resistant suits, including lead-lined inner layers, gloves, and boots rated for 100mSv exposure within 48 hours." The feedback items must be derived *ONLY* from the vague project description. If the task is about software, *avoid describing generic steps such as "procure a library" or describing general safety risks*. Instead, focus on calculations, algorithms, or implementation details *if they are directly implied by the project description*. The actions for handling the risks MUST be extremely explicit, and describe *how to handle the risk* rather than *what the risk is*. You MUST explain *why* a specific action is needed.
225
+ - The **"feedback_title"** should capture the essence of the feedback in **around 7 words** and should imply very specific actions (e.g., "Procure Safety Gear", "Map the Area"). It should not be a general statement, and it should use an active verb. The 'feedback_title' must NOT include the text 'by Date', as this is unnecessary.
226
+ - The feedback items MUST NOT be too high level, and MUST be very specific. The aim should be to provide a checklist that can be rapidly assessed by any project team. The items must be directly executable as a checklist. When describing quantities, always use phrases such as "at least X" or "no more than X" unless the exact amount is known. All timeframes MUST include a specific date AND time, and the time must be expressed as a 24 hour clock using HH:MM format. If a specific action is likely to be difficult to achieve in the timeframe, the response MUST include an alternative action to mitigate this risk. *Use a bulleted list for all steps, do not include numbered lists.*
227
+
228
+ **Combined Summary and Recommendation:**
229
+
230
+ - **"combined_summary":** Summarize the **3 most critical, immediate actions**, selected from the feedback items across *both* experts. Explain *why* these actions are the most immediately essential, and how they mitigate the most important risks. Do *not* reference the feedback item using the expert name and `feedback_index`.
231
+ - **"go_no_go_recommendation":** Provide a clear recommendation of **"Execute Immediately"**, **"Proceed with Caution"**, or **"Do Not Execute"**. Your recommendation must be based on a balanced assessment of the project's potential risks and feasibility, given the limitations of the provided description and the immediate actions outlined by the experts. Do not default to a single recommendation. The response must show that it has actively considered all three options, and show why it is recommending one option over the other two. If you recommend "Proceed with Caution", include the specific actions required for caution. *If you recommend "Do Not Execute", the response MUST provide a very clear and detailed justification about why it is not feasible to proceed, given the risks and the nature of the project, and if no reasonable mitigation strategy can be proposed. The response must be derived from the vague project description, with clear and obvious reasons why the project cannot be executed immediately. You must use examples directly from the description to justify your recommendation, and you must explain what part of the description is not feasible or creates a contradiction*. The recommendation should be a reflection of the overall safety and operational concerns, given the described project. Provide a brief, *concrete* explanation supporting this recommendation, highlighting the primary risks or critical actions that have influenced the decision.
232
+
233
+ **Additional Guidelines:**
234
+
235
+ - Use **consistent, professional, action-oriented language** throughout.
236
+ - **Avoid redundancy** between experts; ensure each expert addresses distinct aspects.
237
+ - Include **specific deadlines** (e.g., "by year-month-day") or timings to emphasize urgency, if necessary.
238
+ - Include **quantifiable details** (e.g., "10 meters of rope", "5 sterile collection tubes") to ensure clarity and measurability.
239
+ - The tone should be that of a professional, who has seen many projects, and therefore immediately recognizes key issues that must be resolved for this project to proceed.
240
+ - Be aware that some timelines may be impossible. If a timeline is unrealistic, the response should provide an alternative approach to obtain those results, rather than just accepting the unrealistic timeframe as given. Do not propose that a government permit can be obtained in a single day.
241
+ """
242
+
243
+ EXPERT_BROAD_SYSTEM_PROMPT = EXPERT_BROAD_SYSTEM_PROMPT_6
244
+
245
+ @dataclass
246
+ class PreProjectAssessment:
247
+ """
248
+ Obtain a broad perspective from 2 experts.
249
+ """
250
+ system_prompt: Optional[str]
251
+ user_prompt: str
252
+ response: dict
253
+ metadata: dict
254
+ preproject_assessment: dict
255
+
256
+ @classmethod
257
+ def execute(cls, llm: LLM, user_prompt: str, **kwargs: Any) -> 'PreProjectAssessment':
258
+ """
259
+ Invoke LLM and have 2 experts take a broad look at the initial plan.
260
+ """
261
+ if not isinstance(llm, LLM):
262
+ raise ValueError("Invalid LLM instance.")
263
+ if not isinstance(user_prompt, str):
264
+ raise ValueError("Invalid query.")
265
+
266
+ # Obtain the current year as a string, eg. "1984"
267
+ current_year_int = datetime.now().year
268
+ current_year = str(current_year_int)
269
+
270
+ # Replace the placeholder in the system prompt with the current year
271
+ system_prompt = EXPERT_BROAD_SYSTEM_PROMPT.strip()
272
+ system_prompt = system_prompt.replace("CURRENT_YEAR_PLACEHOLDER", current_year)
273
+
274
+ default_args = {
275
+ 'system_prompt': system_prompt
276
+ }
277
+ default_args.update(kwargs)
278
+
279
+ system_prompt = default_args.get('system_prompt')
280
+ logger.debug(f"System Prompt:\n{system_prompt}")
281
+ if system_prompt and not isinstance(system_prompt, str):
282
+ raise ValueError("Invalid system prompt.")
283
+
284
+ chat_message_list1 = []
285
+ if system_prompt:
286
+ chat_message_list1.append(
287
+ ChatMessage(
288
+ role=MessageRole.SYSTEM,
289
+ content=system_prompt,
290
+ )
291
+ )
292
+
293
+ logger.debug(f"User Prompt:\n{user_prompt}")
294
+ chat_message_user = ChatMessage(
295
+ role=MessageRole.USER,
296
+ content=user_prompt,
297
+ )
298
+ chat_message_list1.append(chat_message_user)
299
+
300
+ sllm = llm.as_structured_llm(ExpertDetails)
301
+
302
+ logger.debug("Starting LLM chat interaction.")
303
+ start_time = time.perf_counter()
304
+ chat_response1 = sllm.chat(chat_message_list1)
305
+ end_time = time.perf_counter()
306
+ duration = int(ceil(end_time - start_time))
307
+ response_byte_count = len(chat_response1.message.content.encode('utf-8'))
308
+ logger.info(f"LLM chat interaction completed in {duration} seconds. Response byte count: {response_byte_count}")
309
+
310
+ metadata = dict(llm.metadata)
311
+ metadata["llm_classname"] = llm.class_name()
312
+ metadata["duration"] = duration
313
+ metadata["response_byte_count"] = response_byte_count
314
+
315
+ try:
316
+ json_response = json.loads(chat_response1.message.content)
317
+ except json.JSONDecodeError as e:
318
+ logger.error("Failed to parse LLM response as JSON.", exc_info=True)
319
+ raise ValueError("Invalid JSON response from LLM.") from e
320
+
321
+ # Cleanup the json response from the LLM model.
322
+ # Discard the name of the experts and the role of the experts,
323
+ # these are causing confusion downstream when trying to do project planning.
324
+ # Having those names and roles causes the LLM to think they are stakeholders in the project, which they are not.
325
+
326
+ # Extract just the feedback items
327
+ flat_feedback_list = []
328
+ for key in ['expert1', 'expert2']:
329
+ expert = json_response.get(key)
330
+ if expert is None:
331
+ logger.error(f"Expert {key} not found in response.")
332
+ continue
333
+
334
+ for feedback_item in expert.get('feedback_item_list', []):
335
+ flat_feedback_list.append({
336
+ "title": feedback_item.get('feedback_title', 'Empty'),
337
+ "description": feedback_item.get('feedback_description', 'Empty')
338
+ })
339
+ preproject_assessment = {
340
+ 'go_no_go_recommendation': json_response.get('go_no_go_recommendation', 'Empty'),
341
+ 'combined_summary': json_response.get('combined_summary', 'Empty'),
342
+ 'feedback': flat_feedback_list
343
+ }
344
+ logger.info(f"Extracted {len(flat_feedback_list)} feedback items.")
345
+
346
+ result = PreProjectAssessment(
347
+ system_prompt=system_prompt,
348
+ user_prompt=user_prompt,
349
+ response=json_response,
350
+ metadata=metadata,
351
+ preproject_assessment=preproject_assessment
352
+ )
353
+ logger.debug("CreateProjectPlan instance created successfully.")
354
+ return result
355
+
356
+ def to_dict(self, include_metadata=True, include_system_prompt=True, include_user_prompt=True) -> dict:
357
+ d = self.response.copy()
358
+ if include_metadata:
359
+ d['metadata'] = self.metadata
360
+ if include_system_prompt:
361
+ d['system_prompt'] = self.system_prompt
362
+ if include_user_prompt:
363
+ d['user_prompt'] = self.user_prompt
364
+ return d
365
+
366
+ def save_raw(self, file_path: str) -> None:
367
+ with open(file_path, 'w') as f:
368
+ f.write(json.dumps(self.to_dict(), indent=2))
369
+
370
+ def save_preproject_assessment(self, file_path: str) -> None:
371
+ with open(file_path, 'w') as f:
372
+ f.write(json.dumps(self.preproject_assessment, indent=2))
373
+
374
+ if __name__ == "__main__":
375
+ import logging
376
+ from src.llm_factory import get_llm
377
+ from src.plan.find_plan_prompt import find_plan_prompt
378
+
379
+ logging.basicConfig(
380
+ level=logging.DEBUG,
381
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
382
+ handlers=[
383
+ logging.StreamHandler()
384
+ ]
385
+ )
386
+
387
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
388
+ query = (
389
+ f"{plan_prompt}\n\n"
390
+ "Today's date:\n2025-Jan-26\n\n"
391
+ "Project start ASAP"
392
+ )
393
+
394
+ llm = get_llm("ollama-llama3.1")
395
+ # llm = get_llm("deepseek-chat", max_tokens=8192)
396
+
397
+ print(f"Query: {query}")
398
+ result = PreProjectAssessment.execute(llm, query)
399
+
400
+ print("\n\nResponse:")
401
+ print(json.dumps(result.to_dict(include_system_prompt=False, include_user_prompt=False), indent=2))
402
+
403
+ print("\n\nPreproject assessment:")
404
+ print(json.dumps(result.preproject_assessment, indent=2))
src/expert/test_data/solarfarm_expert_list.json ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "a0f6dfa3-45f6-4126-9843-79995b9ea935",
4
+ "title": "Renewable Energy Policy Specialist",
5
+ "knowledge": "Danish renewable energy policies, government incentives, and regulatory frameworks.",
6
+ "why": "To provide insights on the current policy landscape and potential future changes that may impact the solar farm project.",
7
+ "what": "Assessing the likelihood of continued government support for renewable energy projects and identifying potential risks associated with relying on subsidies and incentives.",
8
+ "skills": "Analyzing policy documents, understanding regulatory frameworks, and communicating complex information to stakeholders.",
9
+ "search_query": "Renewable Energy Policy Specialist in Denmark"
10
+ },
11
+ {
12
+ "id": "208da91f-97cb-4915-b1f8-265ce759d13f",
13
+ "title": "Community Engagement Expert",
14
+ "knowledge": "Effective community engagement strategies, local attitudes towards solar farms, and social impact assessments.",
15
+ "why": "To provide guidance on how to engage with the local community, address concerns, and gain support for the project.",
16
+ "what": "Developing a comprehensive community engagement plan, assessing local attitudes towards solar farms, and identifying potential risks associated with negative public perception.",
17
+ "skills": "Community outreach, stakeholder analysis, and conflict resolution.",
18
+ "search_query": "Community Engagement Expert in Denmark"
19
+ },
20
+ {
21
+ "id": "bb34f731-0162-4e09-a1bc-6c62a811ef71",
22
+ "title": "Energy Storage Specialist",
23
+ "knowledge": "Energy storage solutions, their feasibility, and potential for mitigating intermittency.",
24
+ "why": "To provide expertise on the development of an energy storage solution plan and its integration with the solar farm project.",
25
+ "what": "Assessing different energy storage solutions, evaluating their feasibility, and identifying potential risks associated with technological disruptions.",
26
+ "skills": "Energy storage system design, feasibility studies, and technical writing.",
27
+ "search_query": "Energy Storage Specialist in Denmark"
28
+ },
29
+ {
30
+ "id": "fa98fa94-386d-4ed3-b216-d079ffaa276a",
31
+ "title": "Cybersecurity Expert",
32
+ "knowledge": "Solar farm control systems security, potential cybersecurity threats, and mitigation strategies.",
33
+ "why": "To provide guidance on implementing a robust cybersecurity plan to protect the solar farm's control systems.",
34
+ "what": "Developing a comprehensive cybersecurity plan, identifying potential risks associated with cybersecurity threats, and evaluating the effectiveness of mitigation strategies.",
35
+ "skills": "Cybersecurity risk assessment, threat analysis, and incident response planning.",
36
+ "search_query": "Cybersecurity Expert in Denmark"
37
+ },
38
+ {
39
+ "id": "1325be99-ba08-4283-9297-ab8098589d51",
40
+ "title": "Grid Infrastructure Specialist",
41
+ "knowledge": "Danish grid infrastructure capacity, energy distribution optimization, and integration of renewable energy sources.",
42
+ "why": "To provide insights on the grid infrastructure's ability to handle the solar farm's output and identify potential risks associated with integrating the solar farm into the grid.",
43
+ "what": "Assessing the grid infrastructure's capacity to handle the solar farm's output, evaluating the feasibility of smart grid technologies, and identifying potential risks associated with energy distribution disruptions.",
44
+ "skills": "Grid system analysis, energy distribution optimization, and technical writing.",
45
+ "search_query": "Grid Infrastructure Specialist in Denmark"
46
+ },
47
+ {
48
+ "id": "d019beb2-f900-4e43-8ca6-920a24bc7fda",
49
+ "title": "Agrivoltaics Expert",
50
+ "knowledge": "Agricultural applications of solar farms, agrivoltaic systems design, and integration with other sectors (e.g., agriculture, electric vehicle charging infrastructure).",
51
+ "why": "To provide expertise on the development of a 'killer application' by integrating the solar farm with agricultural activities.",
52
+ "what": "Evaluating the feasibility of agrivoltaics, assessing potential revenue streams, and identifying potential risks associated with integrating the solar farm with other sectors.",
53
+ "skills": "Agrivoltaic system design, agricultural applications of solar energy, and business development.",
54
+ "search_query": "Agrivoltaics Expert in Denmark"
55
+ },
56
+ {
57
+ "id": "b8f51b0f-0875-4601-9871-ad94446f11c9",
58
+ "title": "Data Analytics and AI Specialist",
59
+ "knowledge": "Solar farm performance optimization using data analytics and AI, predictive maintenance strategies, and energy storage solution integration.",
60
+ "why": "To provide insights on how to optimize the solar farm's performance using data analytics and AI, and identify potential risks associated with relying on these technologies.",
61
+ "what": "Developing a comprehensive data analytics and AI plan, evaluating the feasibility of predictive maintenance strategies, and identifying potential risks associated with technological disruptions.",
62
+ "skills": "Data analysis, machine learning, and technical writing.",
63
+ "search_query": "Data Analytics and AI Specialist in Denmark"
64
+ },
65
+ {
66
+ "id": "54b7837f-92d1-4e3d-ac54-0228f014a582",
67
+ "title": "Economic Development Expert",
68
+ "knowledge": "Renewable energy project economic viability, cost-benefit analysis of different energy storage solutions, and potential risks associated with market fluctuations.",
69
+ "why": "To provide expertise on the economic feasibility of the solar farm project and identify potential risks associated with market fluctuations.",
70
+ "what": "Conducting a comprehensive cost-benefit analysis, evaluating the economic viability of the solar farm project, and identifying potential risks associated with market disruptions.",
71
+ "skills": "Cost-benefit analysis, market research, and financial modeling.",
72
+ "search_query": "Economic Development Expert in Denmark"
73
+ }
74
+ ]
src/expert/test_data/solarfarm_swot_analysis.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SWOT Analysis
2
+
3
+ ## Topic
4
+ Solar farm in Denmark
5
+
6
+ ## Type
7
+ business
8
+
9
+ ## Type detailed
10
+ Strategic Planning
11
+
12
+ ## Strengths 👍💪🦾
13
+ - Denmark's strong commitment to renewable energy and sustainability.
14
+ - Availability of skilled workforce (engineers, technicians) in the renewable energy sector.
15
+ - Established regulatory framework for renewable energy projects.
16
+ - Potential for government subsidies and incentives for renewable energy projects.
17
+ - Access to advanced technology and equipment for solar farm construction.
18
+ - Strong grid infrastructure capable of integrating renewable energy sources.
19
+
20
+ ## Weaknesses 👎😱🪫⚠️
21
+ - High initial capital investment required.
22
+ - Dependence on weather conditions for energy generation.
23
+ - Potential land use conflicts and environmental concerns.
24
+ - Complexity of navigating Danish regulatory and permitting processes.
25
+ - Fluctuations in solar panel prices and supply chain vulnerabilities.
26
+ - Lack of a 'killer app' or unique selling proposition to differentiate from other solar farms.
27
+
28
+ ## Opportunities 🌈🌐
29
+ - Development of energy storage solutions to mitigate intermittency.
30
+ - Integration of smart grid technologies to optimize energy distribution.
31
+ - Collaboration with local communities to gain social acceptance and support.
32
+ - Expansion of solar farm capacity to meet growing energy demand.
33
+ - Creation of a 'killer application' by integrating the solar farm with other sectors, such as agriculture (agrivoltaics) or electric vehicle charging infrastructure, offering a unique value proposition.
34
+ - Leveraging data analytics and AI to optimize solar farm performance and predictive maintenance.
35
+
36
+ ## Threats ☠️🛑🚨☢︎💩☣︎
37
+ - Changes in government policies and regulations regarding renewable energy.
38
+ - Increased competition from other renewable energy sources (e.g., wind, biomass).
39
+ - Potential for technological disruptions that could render existing solar technology obsolete.
40
+ - Negative public perception due to environmental concerns or visual impact.
41
+ - Economic downturns that could impact investment in renewable energy projects.
42
+ - Cybersecurity threats to the solar farm's control systems.
43
+
44
+ ## Recommendations 💡✅
45
+ - Within 3 months, conduct a comprehensive market analysis to identify potential 'killer applications' such as agrivoltaics or integrated EV charging, led by the Business Development team.
46
+ - Within 6 months, establish partnerships with local communities and environmental organizations to address concerns and gain support, managed by the Stakeholder Relations Manager.
47
+ - Within 12 months, develop a detailed plan for integrating energy storage solutions to mitigate intermittency, overseen by the Engineering Manager.
48
+ - Continuously monitor and adapt to changes in government policies and regulations, with the Legal and Compliance Officer providing monthly updates.
49
+ - Within 18 months, implement a robust cybersecurity plan to protect the solar farm's control systems, led by the IT Security team.
50
+
51
+ ## Strategic Objectives 🎯🔭⛳🏅
52
+ - Achieve 50 MW solar farm operational capacity within 24 months, measured by energy output and grid integration success.
53
+ - Secure all necessary permits and regulatory approvals within 12 months, demonstrated by obtaining required documentation from Danish authorities.
54
+ - Establish positive relationships with local communities, achieving a satisfaction rating of 80% based on community surveys within 18 months.
55
+ - Develop and implement an energy storage solution plan within 12 months, measured by the plan's feasibility and potential for mitigating intermittency.
56
+ - Identify and develop a 'killer application' (e.g., agrivoltaics) within 6 months, measured by the potential for increased revenue and market differentiation.
57
+
58
+ ## Assumptions 🤔🧠🔍
59
+ - The Danish government will continue to support renewable energy projects through subsidies and incentives.
60
+ - The cost of solar panel technology will remain competitive compared to other energy sources.
61
+ - The local community will be receptive to the solar farm project with proper engagement and mitigation of concerns.
62
+ - The grid infrastructure will be capable of handling the additional energy generated by the solar farm.
63
+ - No major technological disruptions will render the planned solar technology obsolete within the project timeline.
64
+
65
+ ## Missing Information 🧩🤷‍♂️🤷‍♀️
66
+ - Detailed analysis of potential 'killer application' opportunities and their market viability.
67
+ - Specific data on community attitudes towards solar farms in the proposed location.
68
+ - Comprehensive assessment of the grid infrastructure's capacity to handle the solar farm's output.
69
+ - Detailed cost-benefit analysis of different energy storage solutions.
70
+ - Up-to-date information on potential changes in Danish renewable energy policies and regulations.
71
+
72
+ ## Questions 🙋❓💬📌
73
+ - What are the most promising 'killer application' opportunities for this solar farm, and what resources are needed to develop them?
74
+ - How can we effectively engage with the local community to address their concerns and gain their support for the project?
75
+ - What are the potential risks associated with relying on government subsidies and incentives, and how can we mitigate them?
76
+ - How can we ensure that the solar farm's technology remains competitive and relevant in the face of potential technological disruptions?
77
+ - What are the most effective strategies for mitigating the environmental impact of the solar farm and ensuring its long-term sustainability?
src/fiction/__init__.py ADDED
File without changes
src/fiction/data/simple_fiction_prompts.jsonl ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {"id": "6448bd67-2e67-4324-a7c6-b59960443e36", "prompt": "The Unified Energy Civilization Grid\n\nGoal: Provide unlimited energy for all humanity.\nConstruct a Dyson Swarm\u2014a system of trillions of solar-collecting satellites orbiting the Sun\u2014to harness nearly all its energy output and beam it to Earth and other planets wirelessly. This would ensure a post-scarcity energy society.\n", "tags": ["space", "dyson swarm", "solar"]}
2
+ {"id": "65974e78-3643-4f84-b323-f71316c2a8b8", "prompt": "What is a project? Work breakdown structure and estimating costs.", "tags": ["project", "WBS", "cost"]}
3
+ {"id": "03d3a59a-2962-4ed9-9e57-7a7e3a989bbd", "prompt": "I have received a SWOT analysis. What should I pay attention to? A guide for project managers.", "tags": ["swot", "guide", "project manager"]}
4
+ {"id": "6957d9c3-f188-4f31-bdd7-ed3ec0f153db", "prompt": "SWOT analysis. Show 20 detailed examples. Business/personal. University education level.", "tags": ["swot", "education"]}
5
+ {"id": "eae2d3c9-aa6d-4e61-a671-3723dcebb50e", "prompt": "out-of-distribution, how to find any anomaly", "tags": ["anomaly", "outlier", "OOD"]}
6
+ {"id": "0e8e9b9d-95dd-4632-b47c-dcc4625a556d", "prompt": "Daily life in a lunar colony", "tags": ["moon", "space"]}
7
+ {"id": "4a2f93db-db24-403a-bad5-5b0ccbea8d58", "prompt": "Planning poker. A guide for project managers.", "tags": ["planning", "project manager", "guide"]}
src/fiction/fiction_writer.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Based on short description, make a longer description.
3
+
4
+ PROMPT> python -m src.fiction.fiction_writer
5
+ """
6
+ import json
7
+ import time
8
+ import logging
9
+ from math import ceil
10
+ from typing import Optional
11
+ from dataclasses import dataclass
12
+ from pydantic import BaseModel, Field
13
+ from llama_index.core.llms import ChatMessage, MessageRole
14
+ from llama_index.core.llms.llm import LLM
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class BookDraft(BaseModel):
19
+ book_title: str = Field(description="Human readable title.")
20
+ overview: str = Field(description="What is this about?")
21
+ elaborate: str = Field(description="Details")
22
+ background_story: str = Field(description="What is the background story.")
23
+ blurb: str = Field(description="The back cover of the book. Immediately capture the readers attention.")
24
+ goal: str = Field(description="What is the goal.")
25
+ main_characters: list[str] = Field(description="List of characters in the story and their background story.")
26
+ character_flaws: list[str] = Field(description="Character flaws relevant to the story.")
27
+ plot_devices: list[str] = Field(description="Items that appear in the story.")
28
+ possible_plot_ideas: list[str] = Field(description="List of story directions.")
29
+ challenges: list[str] = Field(description="Things that could go wrong or be difficult.")
30
+ chapter_title_list: list[str] = Field(description="Name of each chapter.")
31
+ final_story: str = Field(description="Based on the above, what is the final story.")
32
+
33
+ @dataclass
34
+ class FictionWriter:
35
+ """
36
+ Given a short text, elaborate on it.
37
+ """
38
+ query: str
39
+ response: dict
40
+ metadata: dict
41
+
42
+ @classmethod
43
+ def execute(cls, llm: LLM, query: str, system_prompt: Optional[str]) -> 'FictionWriter':
44
+ """
45
+ Invoke LLM to write a fiction based on the query.
46
+ """
47
+ if not isinstance(llm, LLM):
48
+ raise ValueError("Invalid LLM instance.")
49
+ if not isinstance(query, str):
50
+ raise ValueError("Invalid query.")
51
+
52
+ chat_message_list = []
53
+ if system_prompt:
54
+ chat_message_list.append(
55
+ ChatMessage(
56
+ role=MessageRole.SYSTEM,
57
+ content=system_prompt,
58
+ )
59
+ )
60
+
61
+ chat_message_list.append(ChatMessage(
62
+ role=MessageRole.USER,
63
+ content=query
64
+ ))
65
+
66
+ start_time = time.perf_counter()
67
+
68
+ sllm = llm.as_structured_llm(BookDraft)
69
+ try:
70
+ chat_response = sllm.chat(chat_message_list)
71
+ except Exception as e:
72
+ logger.error(f"FictionWriter failed to chat with LLM: {e}")
73
+ raise ValueError(f"Failed to chat with LLM: {e}")
74
+ json_response = json.loads(chat_response.message.content)
75
+
76
+ end_time = time.perf_counter()
77
+ duration = int(ceil(end_time - start_time))
78
+
79
+ metadata = dict(llm.metadata)
80
+ metadata["llm_classname"] = llm.class_name()
81
+ metadata["duration"] = duration
82
+
83
+ result = FictionWriter(
84
+ query=query,
85
+ response=json_response,
86
+ metadata=metadata
87
+ )
88
+ return result
89
+
90
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
91
+ d = self.response.copy()
92
+ if include_metadata:
93
+ d['metadata'] = self.metadata
94
+ if include_query:
95
+ d['query'] = self.query
96
+ return d
97
+
98
+ if __name__ == "__main__":
99
+ from src.llm_factory import get_llm
100
+ from src.prompt.prompt_catalog import PromptCatalog
101
+ import os
102
+
103
+ logging.basicConfig(
104
+ level=logging.DEBUG,
105
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
106
+ handlers=[
107
+ logging.StreamHandler()
108
+ ]
109
+ )
110
+
111
+ system_prompt = "You are a fiction writer that has been given a short description to elaborate on."
112
+ system_prompt = "You are a non-fiction writer that has been given a short description to elaborate on."
113
+
114
+ prompt_catalog = PromptCatalog()
115
+ prompt_catalog.load(os.path.join(os.path.dirname(__file__), 'data', 'simple_fiction_prompts.jsonl'))
116
+ prompt_item = prompt_catalog.find("0e8e9b9d-95dd-4632-b47c-dcc4625a556d")
117
+
118
+ if not prompt_item:
119
+ raise ValueError("Prompt item not found.")
120
+ query = prompt_item.prompt
121
+
122
+ llm = get_llm("ollama-llama3.1") # works
123
+ # llm = get_llm("openrouter-paid-gemini-2.0-flash-001") # works
124
+ # llm = get_llm("ollama-qwen")
125
+ # llm = get_llm("ollama-phi")
126
+ # llm = get_llm("deepseek-chat")
127
+
128
+ print(f"System: {system_prompt}")
129
+ print(f"\n\nQuery: {query}")
130
+ result = FictionWriter.execute(llm, query, system_prompt)
131
+
132
+ print("\n\nResponse:")
133
+ print(json.dumps(result.raw_response_dict(include_query=False), indent=2))
src/format_json_for_use_in_query.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Union, Dict, List, Any
3
+
4
+ # Define a type alias for JSON data that can be either a dict or a list
5
+ JSONType = Union[Dict[str, Any], List[Any]]
6
+
7
+ def format_json_for_use_in_query(d: JSONType) -> str:
8
+ """
9
+ Format a dictionary or list as a JSON string for use in a query.
10
+ We are not interested in the several unwanted fields if it's a dictionary.
11
+ """
12
+ if isinstance(d, dict):
13
+ # Make a copy to avoid mutating the original data
14
+ result_dict = d.copy()
15
+ # Remove unwanted keys if they exist
16
+ result_dict.pop('metadata', None)
17
+ result_dict.pop('query', None)
18
+ result_dict.pop('user_prompt', None)
19
+ result_dict.pop('system_prompt', None)
20
+ return json.dumps(result_dict, separators=(',', ':'))
21
+ elif isinstance(d, list):
22
+ # If it's a list, simply convert it to a JSON string
23
+ return json.dumps(d, separators=(',', ':'))
24
+ else:
25
+ raise TypeError("Input must be a dictionary or a list")
26
+
27
+ if __name__ == "__main__":
28
+ data_dict = {
29
+ 'key1': 'value1',
30
+ 'key2': 'value2',
31
+ 'query': 'long text input from the user',
32
+ 'metadata': {
33
+ 'duration': 42,
34
+ }
35
+ }
36
+ expected_dict = '{"key1":"value1","key2":"value2"}'
37
+ assert format_json_for_use_in_query(data_dict) == expected_dict
38
+
39
+ data_list = [
40
+ 'item1',
41
+ 'item2',
42
+ 'item3',
43
+ ]
44
+ expected_list = '["item1","item2","item3"]'
45
+ assert format_json_for_use_in_query(data_list) == expected_list
46
+ print('PASSED')
src/llm_factory.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from dotenv import dotenv_values
4
+ from typing import Optional, Any, Dict
5
+ from llama_index.core.llms.llm import LLM
6
+ from llama_index.llms.ollama import Ollama
7
+ from llama_index.llms.openai_like import OpenAILike
8
+ from llama_index.llms.together import TogetherLLM
9
+ from llama_index.llms.groq import Groq
10
+ from llama_index.llms.lmstudio import LMStudio
11
+ from llama_index.llms.openrouter import OpenRouter
12
+
13
+ __all__ = ["get_llm", "get_available_llms"]
14
+
15
+ # Define paths and load environment variables and config
16
+ _dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env"))
17
+ _dotenv_dict = dotenv_values(dotenv_path=_dotenv_path)
18
+ _config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "llm_config.json"))
19
+
20
+
21
+ def load_config(config_path: str) -> Dict[str, Any]:
22
+ """Loads the configuration from a JSON file."""
23
+ try:
24
+ with open(config_path, "r") as f:
25
+ return json.load(f)
26
+ except FileNotFoundError:
27
+ print(f"Warning: config.json not found at {config_path}. Using default settings.")
28
+ return {}
29
+ except json.JSONDecodeError as e:
30
+ raise ValueError(f"Error decoding JSON from {config_path}: {e}")
31
+
32
+
33
+ _llm_configs = load_config(_config_path)
34
+
35
+
36
+ def substitute_env_vars(config: Dict[str, Any], env_vars: Dict[str, str]) -> Dict[str, Any]:
37
+ """Recursively substitutes environment variables in the configuration."""
38
+
39
+ def replace_value(value: Any) -> Any:
40
+ if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
41
+ var_name = value[2:-1] # Extract variable name
42
+ if var_name in env_vars:
43
+ return env_vars[var_name]
44
+ else:
45
+ print(f"Warning: Environment variable '{var_name}' not found.")
46
+ return value # Or raise an error if you prefer strict enforcement
47
+ return value
48
+
49
+ def process_item(item):
50
+ if isinstance(item, dict):
51
+ return {k: process_item(v) for k, v in item.items()}
52
+ elif isinstance(item, list):
53
+ return [process_item(i) for i in item]
54
+ else:
55
+ return replace_value(item)
56
+
57
+ return process_item(config)
58
+
59
+ def get_available_llms() -> list[str]:
60
+ """
61
+ Returns a list of available LLM names.
62
+ """
63
+ return list(_llm_configs.keys())
64
+
65
+ def get_llm(llm_name: Optional[str] = None, **kwargs: Any) -> LLM:
66
+ """
67
+ Returns an LLM instance based on the config.json file or a fallback default.
68
+
69
+ :param llm_name: The name/key of the LLM to instantiate.
70
+ If None, falls back to DEFAULT_LLM in .env (or 'ollama-llama3.1').
71
+ :param kwargs: Additional keyword arguments to override default model parameters.
72
+ :return: An instance of a LlamaIndex LLM class.
73
+ """
74
+ if not llm_name:
75
+ llm_name = _dotenv_dict.get("DEFAULT_LLM", "ollama-llama3.1")
76
+
77
+ if llm_name not in _llm_configs:
78
+ # If llm_name doesn't exits in _llm_configs, then we go through default settings
79
+ print(f"Warning: LLM '{llm_name}' not found in config.json. Falling back to hardcoded defaults.")
80
+ raise ValueError(f"Unsupported LLM name: {llm_name}")
81
+
82
+ config = _llm_configs[llm_name]
83
+ class_name = config.get("class")
84
+ arguments = config.get("arguments", {})
85
+
86
+ # Substitute environment variables
87
+ arguments = substitute_env_vars(arguments, _dotenv_dict)
88
+
89
+ # Override with any kwargs passed to get_llm()
90
+ arguments.update(kwargs)
91
+
92
+ # Dynamically instantiate the class
93
+ try:
94
+ llm_class = globals()[class_name] # Get class from global scope
95
+ return llm_class(**arguments)
96
+ except KeyError:
97
+ raise ValueError(f"Invalid LLM class name in config.json: {class_name}")
98
+ except TypeError as e:
99
+ raise ValueError(f"Error instantiating {class_name} with arguments: {e}")
100
+
101
+ if __name__ == '__main__':
102
+ try:
103
+ llm = get_llm(llm_name="ollama-llama3.1")
104
+ print(f"Successfully loaded LLM: {llm.__class__.__name__}")
105
+ print(llm.complete("Hello, how are you?"))
106
+ except ValueError as e:
107
+ print(f"Error: {e}")
src/plan/README.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # Plan
2
+
3
+ From a project description, create a project plan.
4
+
src/plan/__init__.py ADDED
File without changes
src/plan/app_text2plan.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.plan.app_text2plan
3
+ """
4
+ import gradio as gr
5
+ import os
6
+ import subprocess
7
+ import time
8
+ import sys
9
+ import threading
10
+ from datetime import datetime
11
+ from math import ceil
12
+ from src.llm_factory import get_available_llms
13
+ from src.plan.filenames import FilenameEnum
14
+ from src.plan.plan_file import PlanFile
15
+ from src.plan.speedvsdetail import SpeedVsDetailEnum
16
+ from src.prompt.prompt_catalog import PromptCatalog
17
+ from src.utils.get_env_as_string import get_env_as_string
18
+ from src.utils.time_since_last_modification import time_since_last_modification
19
+
20
+ # Global variables
21
+ active_proc = None
22
+ stop_event = threading.Event()
23
+ latest_run_id = None
24
+ latest_run_dir = None
25
+
26
+ MODULE_PATH_PIPELINE = "src.plan.run_plan_pipeline"
27
+ DEFAULT_PROMPT_UUID = "4dc34d55-0d0d-4e9d-92f4-23765f49dd29"
28
+
29
+ # The pipeline outputs are stored in a log.txt file in the run directory.
30
+ # So we don't need to relay the process output to the console.
31
+ # Except for debugging purposes, you can set this to True.
32
+ RELAY_PROCESS_OUTPUT = False
33
+
34
+ prompt_catalog = PromptCatalog()
35
+ prompt_catalog.load(os.path.join(os.path.dirname(__file__), 'data', 'simple_plan_prompts.jsonl'))
36
+
37
+ # Prefill the input box with the default prompt
38
+ default_prompt_item = prompt_catalog.find(DEFAULT_PROMPT_UUID)
39
+ if default_prompt_item:
40
+ gradio_default_example = default_prompt_item.prompt
41
+ else:
42
+ raise ValueError("DEFAULT_PROMPT_UUID prompt not found.")
43
+
44
+ # Show all prompts in the catalog as examples
45
+ all_prompts = prompt_catalog.all()
46
+ gradio_examples = []
47
+ for prompt_item in all_prompts:
48
+ gradio_examples.append([prompt_item.prompt])
49
+
50
+ available_models = get_available_llms()
51
+ available_model_names = [model for model in available_models]
52
+
53
+ def has_pipeline_complete_file(path_dir: str):
54
+ """
55
+ Checks if the pipeline has completed by looking for the completion file.
56
+ """
57
+ files = os.listdir(path_dir)
58
+ return FilenameEnum.PIPELINE_COMPLETE.value in files
59
+
60
+ class MarkdownBuilder:
61
+ """
62
+ Helper class to build Markdown-formatted strings.
63
+ """
64
+ def __init__(self):
65
+ self.rows = []
66
+
67
+ def add_line(self, line: str):
68
+ self.rows.append(line)
69
+
70
+ def add_code_block(self, code: str):
71
+ self.rows.append("```\n" + code + "\n```")
72
+
73
+ def status(self, status_message: str):
74
+ self.add_line(f"### Status")
75
+ self.add_line(status_message)
76
+
77
+ def path_to_run_dir(self, absolute_path_to_run_dir: str):
78
+ self.add_line(f"### Output dir")
79
+ self.add_code_block(absolute_path_to_run_dir)
80
+
81
+ def list_files(self, path_dir: str):
82
+ self.add_line(f"### Output files")
83
+ files = os.listdir(path_dir)
84
+ files.sort()
85
+ filenames = "\n".join(files)
86
+ self.add_code_block(filenames)
87
+
88
+ def to_markdown(self):
89
+ return "\n".join(self.rows)
90
+
91
+ def run_planner(submit_or_retry_button: str, plan_prompt: str, llm_model: str, speedvsdetail: str):
92
+ """
93
+ This generator function:
94
+
95
+ 1. Prepare the run directory.
96
+ 2. Updates environment with RUN_ID and LLM_MODEL.
97
+ 3. Launches the Python pipeline script.
98
+ 5. Polls the output dir (every second) to yield a Markdown-formatted file listing.
99
+ If a stop is requested or if the pipeline-complete file is detected, it exits.
100
+ """
101
+ global active_proc, stop_event, latest_run_dir, latest_run_id
102
+
103
+ # Clear any previous stop signal.
104
+ stop_event.clear()
105
+
106
+ submit_or_retry = submit_or_retry_button.lower()
107
+
108
+ if submit_or_retry == "retry":
109
+ if latest_run_id is None:
110
+ raise ValueError("No previous run to retry. Please submit a plan first.")
111
+
112
+ run_id = latest_run_id
113
+ run_path = os.path.join("run", run_id)
114
+ absolute_path_to_run_dir = latest_run_dir
115
+
116
+ print(f"Retrying the run with ID: {run_id}")
117
+
118
+ if not os.path.exists(run_path):
119
+ raise Exception(f"The run path is supposed to exist from an earlier run. However the no run path exists: {run_path}")
120
+
121
+ elif submit_or_retry == "submit":
122
+ run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
123
+ run_path = os.path.join("run", run_id)
124
+ absolute_path_to_run_dir = os.path.abspath(run_path)
125
+
126
+ print(f"Submitting a new run with ID: {run_id}")
127
+
128
+ # Update the global variables.
129
+ latest_run_id = run_id
130
+ latest_run_dir = absolute_path_to_run_dir
131
+
132
+ # Prepare a new run directory.
133
+ if os.path.exists(run_path):
134
+ raise Exception(f"The run path is not supposed to exist at this point. However the run path already exists: {run_path}")
135
+ os.makedirs(run_path)
136
+
137
+ # Create the initial plan file.
138
+ plan_file = PlanFile.create(plan_prompt)
139
+ plan_file.save(os.path.join(run_path, FilenameEnum.INITIAL_PLAN.value))
140
+
141
+ # Set the prompt in the environment so the pipeline can use it.
142
+ env = os.environ.copy()
143
+ env["RUN_ID"] = run_id
144
+ env["LLM_MODEL"] = llm_model
145
+ env["SPEED_VS_DETAIL"] = speedvsdetail
146
+
147
+ start_time = time.perf_counter()
148
+
149
+ # Launch the pipeline as a separate Python process.
150
+ command = [sys.executable, "-m", MODULE_PATH_PIPELINE]
151
+ print(f"Executing command: {' '.join(command)}")
152
+ # Spawn threads to print process output to the console.
153
+ if RELAY_PROCESS_OUTPUT:
154
+ active_proc = subprocess.Popen(
155
+ command,
156
+ cwd=".",
157
+ env=env,
158
+ stdout=None,
159
+ stderr=None
160
+ )
161
+ else:
162
+ active_proc = subprocess.Popen(
163
+ command,
164
+ cwd=".",
165
+ env=env,
166
+ stdout=subprocess.DEVNULL,
167
+ stderr=subprocess.DEVNULL
168
+ )
169
+
170
+ # Obtain process id
171
+ child_process_id = active_proc.pid
172
+ print(f"Process started. Process ID: {child_process_id}")
173
+
174
+ # Poll the output directory every second.
175
+ while True:
176
+ # Check if the process has ended.
177
+ if active_proc.poll() is not None:
178
+ break
179
+
180
+ # print("running...")
181
+ end_time = time.perf_counter()
182
+ duration = int(ceil(end_time - start_time))
183
+
184
+ # Check if a stop was requested.
185
+ if stop_event.is_set():
186
+ try:
187
+ active_proc.terminate()
188
+ except Exception as e:
189
+ print("Error terminating process:", e)
190
+
191
+ markdown_builder = MarkdownBuilder()
192
+ markdown_builder.status("Process terminated by user.")
193
+ markdown_builder.path_to_run_dir(absolute_path_to_run_dir)
194
+ markdown_builder.list_files(run_path)
195
+ yield markdown_builder.to_markdown()
196
+ break
197
+
198
+ last_update = ceil(time_since_last_modification(run_path))
199
+ markdown_builder = MarkdownBuilder()
200
+ markdown_builder.status(f"Working. {duration} seconds elapsed. Last output update was {last_update} seconds ago.")
201
+ markdown_builder.path_to_run_dir(absolute_path_to_run_dir)
202
+ markdown_builder.list_files(run_path)
203
+ yield markdown_builder.to_markdown()
204
+
205
+ # If the pipeline complete file is found, finish streaming.
206
+ if has_pipeline_complete_file(run_path):
207
+ break
208
+
209
+ time.sleep(1)
210
+
211
+ # Wait for the process to end (if it hasn't already) and clear our global.
212
+ returncode = 'NOT SET'
213
+ if active_proc is not None:
214
+ active_proc.wait()
215
+ returncode = active_proc.returncode
216
+ active_proc = None
217
+
218
+ # Process has completed.
219
+ end_time = time.perf_counter()
220
+ duration = int(ceil(end_time - start_time))
221
+ print(f"Process ended. returncode: {returncode}. Process ID: {child_process_id}. Duration: {duration} seconds.")
222
+
223
+ if has_pipeline_complete_file(run_path):
224
+ status_message = "Completed."
225
+ else:
226
+ status_message = "Stopped prematurely, the output may be incomplete."
227
+
228
+ # Final file listing update.
229
+ markdown_builder = MarkdownBuilder()
230
+ markdown_builder.status(f"{status_message} {duration} seconds elapsed.")
231
+ markdown_builder.path_to_run_dir(absolute_path_to_run_dir)
232
+ markdown_builder.list_files(run_path)
233
+ yield markdown_builder.to_markdown()
234
+
235
+ def stop_planner():
236
+ """
237
+ Sets a global stop flag and, if there is an active process, attempts to terminate it.
238
+ Returns a status message.
239
+ """
240
+ global active_proc, stop_event
241
+ stop_event.set()
242
+ if active_proc is not None:
243
+ try:
244
+ if active_proc.poll() is None: # Process is still running.
245
+ active_proc.terminate()
246
+ return "Stop signal sent. Process termination requested."
247
+ except Exception as e:
248
+ return f"Error terminating process: {e}"
249
+ else:
250
+ return "No active process to stop."
251
+
252
+ def open_output_dir():
253
+ """
254
+ Opens the latest output directory in the native file explorer.
255
+ """
256
+ if not latest_run_dir or not os.path.exists(latest_run_dir):
257
+ return "No output directory available."
258
+
259
+ try:
260
+ if sys.platform == "darwin": # macOS
261
+ subprocess.Popen(["open", latest_run_dir])
262
+ elif sys.platform == "win32": # Windows
263
+ subprocess.Popen(["explorer", latest_run_dir])
264
+ else: # Assume Linux or other unix-like OS
265
+ subprocess.Popen(["xdg-open", latest_run_dir])
266
+ return f"Opened the directory: {latest_run_dir}"
267
+ except Exception as e:
268
+ return f"Failed to open directory: {e}"
269
+
270
+ # Build the Gradio UI using Blocks.
271
+ with gr.Blocks(title="PlanExe") as demo:
272
+ gr.Markdown("# PlanExe: crack open pandora’s box of ideas")
273
+ with gr.Tab("Main"):
274
+ with gr.Row():
275
+ with gr.Column(scale=2, min_width=300):
276
+ prompt_input = gr.Textbox(
277
+ label="Plan Description",
278
+ lines=5,
279
+ placeholder="Enter a description of your plan...",
280
+ value=gradio_default_example
281
+ )
282
+ with gr.Row():
283
+ submit_btn = gr.Button("Submit", variant='primary')
284
+ stop_btn = gr.Button("Stop")
285
+ retry_btn = gr.Button("Retry")
286
+ open_dir_btn = gr.Button("Open Output Dir")
287
+
288
+ output_markdown = gr.Markdown("Output will appear here...")
289
+ status_markdown = gr.Markdown("Status messages will appear here...")
290
+
291
+ with gr.Column(scale=1, min_width=300):
292
+ examples = gr.Examples(
293
+ examples=gradio_examples,
294
+ inputs=[prompt_input],
295
+ )
296
+
297
+ with gr.Tab("Settings"):
298
+ model_radio = gr.Radio(
299
+ available_model_names,
300
+ value=available_model_names[0],
301
+ label="Model",
302
+ interactive=True
303
+ )
304
+
305
+ speedvsdetail_items = [
306
+ ("All details, but slow", SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW),
307
+ ("Fast, but few details", SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS),
308
+ ]
309
+ speedvsdetail_radio = gr.Radio(
310
+ speedvsdetail_items,
311
+ value=SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW,
312
+ label="Speed vs Detail",
313
+ interactive=True
314
+ )
315
+
316
+ with gr.Tab("Join the community"):
317
+ gr.Markdown("""
318
+ - [GitHub](https://github.com/neoneye/PlanExe) the source code.
319
+ - [Discord](https://neoneye.github.io/PlanExe-web/discord) join the community. Suggestions, feedback, and questions are welcome.
320
+ """)
321
+
322
+ # Start the planning assistant with streaming updates.
323
+ submit_btn.click(fn=run_planner, inputs=[submit_btn, prompt_input, model_radio, speedvsdetail_radio], outputs=output_markdown)
324
+ # The stop button simply calls stop_planner and displays its status message.
325
+ stop_btn.click(fn=stop_planner, outputs=status_markdown)
326
+ retry_btn.click(fn=run_planner, inputs=[retry_btn, prompt_input, model_radio, speedvsdetail_radio], outputs=output_markdown)
327
+ open_dir_btn.click(fn=open_output_dir, outputs=status_markdown)
328
+
329
+ # print("Environment variables Gradio:\n" + get_env_as_string() + "\n\n\n")
330
+
331
+ print("Press Ctrl+C to exit.")
332
+ demo.launch()
src/plan/create_pitch.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Create a pitch for this project.
3
+ """
4
+ import os
5
+ import json
6
+ import time
7
+ from math import ceil
8
+ from typing import List, Optional
9
+ from uuid import uuid4
10
+ from dataclasses import dataclass
11
+ from pydantic import BaseModel, Field
12
+ from llama_index.core.llms.llm import LLM
13
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
14
+
15
+ class ProjectPitch(BaseModel):
16
+ pitch: str = Field(
17
+ description="A compelling pitch for this project."
18
+ )
19
+ why_this_pitch_works: str = Field(
20
+ description="Explanation why this pitch works."
21
+ )
22
+ target_audience: str = Field(
23
+ description="Who this pitch is aimed at, such as investors, stakeholders, or the general public."
24
+ )
25
+ call_to_action: str = Field(
26
+ description="A clear next step for the audience to engage with the project."
27
+ )
28
+ risks_and_mitigation: str = Field(
29
+ description="Address potential challenges and demonstrate readiness to handle them."
30
+ )
31
+ metrics_for_success: str = Field(
32
+ description="Define how the success of the project will be measured beyond its goals."
33
+ )
34
+ stakeholder_benefits: str = Field(
35
+ description="Explicitly state what stakeholders gain from supporting or being involved in the project."
36
+ )
37
+ ethical_considerations: str = Field(
38
+ description="Build trust by showing a commitment to ethical practices."
39
+ )
40
+ collaboration_opportunities: str = Field(
41
+ description="Highlight ways other organizations or individuals can partner with the project."
42
+ )
43
+ long_term_vision: str = Field(
44
+ description="Show the broader impact and sustainability of the project."
45
+ )
46
+
47
+ QUERY_PREAMBLE = f"""
48
+ Craft a compelling pitch for this project that starts with an attention-grabbing hook,
49
+ presents its purpose clearly, and highlights the benefits or value it brings. Use a tone
50
+ that conveys enthusiasm and aligns with the goals and values of the intended audience,
51
+ emphasizing why this project matters and how it stands out.
52
+
53
+ """
54
+
55
+ @dataclass
56
+ class CreatePitch:
57
+ query: str
58
+ response: dict
59
+ metadata: dict
60
+
61
+ @classmethod
62
+ def format_query(cls, plan_json: dict, wbs_level1_json: dict, wbs_level2_json: list) -> str:
63
+ """
64
+ Format the query for creating project pitch.
65
+ """
66
+ if not isinstance(plan_json, dict):
67
+ raise ValueError("Invalid plan_json.")
68
+ if not isinstance(wbs_level1_json, dict):
69
+ raise ValueError("Invalid wbs_level1_json.")
70
+ if not isinstance(wbs_level2_json, list):
71
+ raise ValueError("Invalid wbs_level2_json.")
72
+
73
+ query = f"""
74
+ The project plan:
75
+ {format_json_for_use_in_query(plan_json)}
76
+
77
+ WBS Level 1:
78
+ {format_json_for_use_in_query(wbs_level1_json)}
79
+
80
+ WBS Level 2:
81
+ {format_json_for_use_in_query(wbs_level2_json)}
82
+ """
83
+ return query
84
+
85
+ @classmethod
86
+ def execute(cls, llm: LLM, query: str) -> 'CreatePitch':
87
+ """
88
+ Invoke LLM to create a project pitch.
89
+ """
90
+ if not isinstance(llm, LLM):
91
+ raise ValueError("Invalid LLM instance.")
92
+ if not isinstance(query, str):
93
+ raise ValueError("Invalid query.")
94
+
95
+ start_time = time.perf_counter()
96
+
97
+ sllm = llm.as_structured_llm(ProjectPitch)
98
+ response = sllm.complete(QUERY_PREAMBLE + query)
99
+ json_response = json.loads(response.text)
100
+
101
+ end_time = time.perf_counter()
102
+ duration = int(ceil(end_time - start_time))
103
+
104
+ metadata = dict(llm.metadata)
105
+ metadata["llm_classname"] = llm.class_name()
106
+ metadata["duration"] = duration
107
+
108
+ result = CreatePitch(
109
+ query=query,
110
+ response=json_response,
111
+ metadata=metadata,
112
+ )
113
+ return result
114
+
115
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
116
+ d = self.response.copy()
117
+ if include_metadata:
118
+ d['metadata'] = self.metadata
119
+ if include_query:
120
+ d['query'] = self.query
121
+ return d
122
+
123
+ if __name__ == "__main__":
124
+ from llama_index.llms.ollama import Ollama
125
+
126
+ # TODO: Eliminate hardcoded paths
127
+ basepath = '/Users/neoneye/Desktop/planexe_data'
128
+
129
+ def load_json(relative_path: str) -> dict:
130
+ path = os.path.join(basepath, relative_path)
131
+ print(f"loading file: {path}")
132
+ with open(path, 'r', encoding='utf-8') as f:
133
+ the_json = json.load(f)
134
+ return the_json
135
+
136
+ plan_json = load_json('002-project_plan.json')
137
+ wbs_level1_json = load_json('004-wbs_level1.json')
138
+ wbs_level2_json = load_json('005-wbs_level2.json')
139
+
140
+ model_name = "llama3.1:latest"
141
+ # model_name = "qwen2.5-coder:latest"
142
+ # model_name = "phi4:latest"
143
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
144
+
145
+ query = CreatePitch.format_query(plan_json, wbs_level1_json, wbs_level2_json)
146
+ print(f"Query: {query}")
147
+ result = CreatePitch.execute(llm, query)
148
+
149
+ print("\nResponse:")
150
+ json_response = result.raw_response_dict(include_query=False)
151
+ print(json.dumps(json_response, indent=2))
src/plan/create_project_plan.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.plan.create_project_plan
3
+
4
+ Based on a vague description, the creates a rough draft for a project plan.
5
+ """
6
+ import json
7
+ import time
8
+ import logging
9
+ from math import ceil
10
+ from typing import List, Optional, Any, Type, TypeVar
11
+ from dataclasses import dataclass
12
+ from pydantic import BaseModel, Field, ValidationError
13
+ from llama_index.core.llms.llm import LLM
14
+ from llama_index.core.llms import ChatMessage, MessageRole
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class SMARTCriteria(BaseModel):
19
+ specific: str = Field(
20
+ description="Clearly defines what is to be accomplished. Provides context and purpose behind the goal. Avoid vagueness, unrealistic targets, broad statements that lack direction."
21
+ )
22
+ measurable: str = Field(
23
+ description="Establish how you will measure success. This could be quantitative (e.g., numbers, percentages) or qualitative (e.g., satisfaction levels). Without clear metrics, it’s difficult to track progress or determine success."
24
+ )
25
+ achievable: str = Field(
26
+ description="Realism: Ensures the goal is attainable with the available resources and within constraints. Feasibility: Considers practical aspects such as time, budget, and expertise."
27
+ )
28
+ relevant: str = Field(
29
+ description="Alignment: Connects the goal to broader objectives or missions. Importance: Highlights the significance and impact of achieving the goal."
30
+ )
31
+ time_bound: str = Field(
32
+ description="Deadline: Specifies a clear timeframe for goal completion. Milestones: Breaks down the timeline into smaller, manageable phases if necessary."
33
+ )
34
+
35
+ class RiskAssessmentAndMitigationStrategies(BaseModel):
36
+ """
37
+ By systematically identifying, analyzing, and addressing risks, you enhance the project
38
+ resilience and increase the likelihood of achieving sustainable and long-term success.
39
+ """
40
+ key_risks: list[str] = Field(
41
+ description="Things that can negatively impact the project, such as regulatory changes, financial uncertainties, technological failures, supply chain disruptions and environmental challenges."
42
+ )
43
+ diverse_risks: list[str] = Field(
44
+ description="Things that can negatively impact the project, such as operational risks, systematic risks, business risks."
45
+ )
46
+ mitigation_plans: list[str] = Field(
47
+ description="Develop strategies to minimize or manage these risks, ensuring the project remains on track despite unforeseen obstacles."
48
+ )
49
+
50
+ class StakeholderAnalysis(BaseModel):
51
+ """
52
+ Understanding the interests, expectations, and concerns of stakeholders is essential for building
53
+ strong relationships, securing support, and ensuring project success.
54
+ """
55
+ primary_stakeholders: list[str] = Field(
56
+ description="List all key stakeholders, including government agencies, local communities, investors, suppliers, and end-users."
57
+ )
58
+ secondary_stakeholders: list[str] = Field(
59
+ description="List all secondary stakeholders, such as regulatory bodies, environmental groups, and suppliers."
60
+ )
61
+ engagement_strategies: list[str] = Field(
62
+ description="Outline how each stakeholder group will be engaged, their roles, and how their interests will be addressed to secure support and collaboration."
63
+ )
64
+
65
+ class RegulatoryAndComplianceRequirements(BaseModel):
66
+ """
67
+ Detailed overview of the regulatory and compliance requirements necessary for the project.
68
+ Ensures the project operates within legal frameworks and adheres to all necessary standards.
69
+ """
70
+ permits_and_licenses: list[str] = Field(
71
+ description="List of permits and licenses required at local, regional, and national levels. Such as Building Permit, Cave Exploration Permit, Hazardous Materials Handling Permit."
72
+ )
73
+ compliance_standards: list[str] = Field(
74
+ description="Ensure adherence to environmental, safety, and industry-specific standards and regulations."
75
+ )
76
+ regulatory_bodies: list[str] = Field(
77
+ description="List of regulatory bodies and their roles in the project's compliance landscape. Such as International Energy Agency, International Maritime Organization, World Health Organization."
78
+ )
79
+ compliance_actions: list[str] = Field(
80
+ description="List with actions and steps taken to ensure compliance with all relevant regulations and standards. Example: ['Building and Electrical Codes', 'Wildlife Protection', 'Fire safety measures', 'Biosafety Regulations', 'Radiation Safety']."
81
+ )
82
+
83
+ class GoalDefinition(BaseModel):
84
+ """
85
+ A clear, specific, and actionable statement that outlines what you aim to achieve.
86
+ A well-defined goal serves as a foundation for effective planning, problem decomposition, and team assembly.
87
+ """
88
+ goal_statement: str = Field(
89
+ description="Adhering to the SMART criteria (Specific, Measurable, Achievable, Relevant, Time-bound)"
90
+ )
91
+ smart_criteria: SMARTCriteria = Field(
92
+ description="Details of the SMART criteria."
93
+ )
94
+ dependencies: list[str] = Field(
95
+ description="Other goals or tasks that must be completed before this goal can be achieved. Example: ['Securing funding for equipment and personnel', 'Obtaining necessary permits for cave exploration']."
96
+ )
97
+ resources_required: list[str] = Field(
98
+ description="Resources necessary to achieve the goal. Example: ['Cave exploration gear', 'Biological sampling tools']."
99
+ )
100
+ related_goals: list[str] = Field(
101
+ description="Other goals that are related or interconnected. Facilitates understanding of goal interdependencies and broader objectives."
102
+ )
103
+ tags: list[str] = Field(
104
+ description="Keywords or labels associated with the goal. Enhances searchability and categorization, making it easier to filter and find goals based on keywords. Example: ['Extremophiles', 'DNA Sequencing', 'Microbial Life']."
105
+ )
106
+ risk_assessment_and_mitigation_strategies: RiskAssessmentAndMitigationStrategies = Field(
107
+ description="Identify potential risks and develop strategies to mitigate them, ensuring the goal remains achievable despite unforeseen challenges."
108
+ )
109
+ stakeholder_analysis: StakeholderAnalysis = Field(
110
+ description="Analyze key stakeholders, their interests, and engagement strategies to secure support and collaboration for goal achievement."
111
+ )
112
+ regulatory_and_compliance_requirements: RegulatoryAndComplianceRequirements = Field(
113
+ description="Ensure compliance with all regulatory and legal requirements, including permits, licenses, and industry-specific standards."
114
+ )
115
+
116
+ CREATE_PROJECT_PLAN_SYSTEM_PROMPT_1 = """
117
+ You are an expert project planner tasked with creating comprehensive and detailed project plans based on user-provided descriptions. Your output must be a complete JSON object conforming to the provided GoalDefinition schema. Focus on being specific and actionable, generating a plan that is realistic and useful for guiding project development.
118
+ Your plans must include:
119
+ - A clear goal statement adhering to the SMART criteria (Specific, Measurable, Achievable, Relevant, Time-bound). Provide specific metrics and timeframes where possible.
120
+ - A breakdown of dependencies and required resources for the project.
121
+ - A clear identification of related goals and future applications.
122
+ - A detailed risk assessment with specific mitigation strategies. Focus on actionable items to mitigate the risks you have identified.
123
+ - A comprehensive stakeholder analysis, identifying primary and secondary stakeholders, and outlining engagement strategies.
124
+ - A detailed overview of regulatory and compliance requirements, such as permits and licenses, and how compliance actions are planned.
125
+ - Tags or keywords that allow users to easily find and categorize the project.
126
+ Prioritize feasibility, practicality, and alignment with the user-provided description. Ensure the plan is actionable, with concrete steps where possible and measurable outcomes.
127
+ """
128
+
129
+ CREATE_PROJECT_PLAN_SYSTEM_PROMPT_2 = """
130
+ You are an expert project planner tasked with creating comprehensive and detailed project plans based on user-provided descriptions. Your output must be a complete JSON object conforming to the provided GoalDefinition schema. Focus on being specific and actionable, generating a plan that is realistic and useful for guiding project development.
131
+
132
+ Your plans must include:
133
+ - A clear goal statement adhering to the SMART criteria (Specific, Measurable, Achievable, Relevant, Time-bound). Provide specific metrics and timeframes where possible.
134
+ - A breakdown of dependencies and required resources for the project. Break down dependencies into actionable sub-tasks where applicable.
135
+ - A clear identification of related goals and future applications.
136
+ - A detailed risk assessment with specific mitigation strategies. Focus on actionable items to mitigate the risks you have identified, ensuring they are tailored to the project's context.
137
+ - A comprehensive stakeholder analysis, identifying primary and secondary stakeholders, and outlining engagement strategies.
138
+ - **Primary Stakeholders:** Identify key roles or individuals directly responsible for executing the project. For small-scale or personal projects, this may simply be the person performing the task (e.g., "Coffee Brewer"). For large-scale projects, identify domain-specific roles (e.g., "Construction Manager," "Life Support Systems Engineer").
139
+ - **Secondary Stakeholders:** Identify external parties or collaborators relevant to the project. For small-scale projects, this may include suppliers or individuals indirectly affected by the project (e.g., "Coffee Supplier," "Household Members"). For large-scale projects, include regulatory bodies, material suppliers, or other external entities.
140
+ - **Note:** Do not assume the availability or involvement of any specific individuals unless explicitly stated in the user-provided description.
141
+ - A detailed overview of regulatory and compliance requirements, such as permits and licenses, and how compliance actions are planned.
142
+ - Tags or keywords that allow users to easily find and categorize the project.
143
+
144
+ **Adaptive Behavior:**
145
+ - Automatically adjust the level of detail and formality based on the scale and complexity of the project. For small-scale or personal projects, keep the plan simple and practical. For large-scale or complex projects, include more detailed and formal elements.
146
+ - Infer the appropriate stakeholders, risks, and resources based on the project's domain and context. Avoid overly formal or mismatched roles unless explicitly required by the project's context.
147
+
148
+ Prioritize feasibility, practicality, and alignment with the user-provided description. Ensure the plan is actionable, with concrete steps where possible and measurable outcomes.
149
+ """
150
+
151
+ CREATE_PROJECT_PLAN_SYSTEM_PROMPT_3 = """
152
+ You are an expert project planner tasked with creating comprehensive and detailed project plans based on user-provided descriptions. Your output must be a complete JSON object conforming to the provided GoalDefinition schema. Focus on being specific and actionable, generating a plan that is realistic and useful for guiding project development.
153
+
154
+ Your plans must include:
155
+ - A clear goal statement adhering to the SMART criteria (Specific, Measurable, Achievable, Relevant, Time-bound). Provide specific metrics and timeframes where possible. For the time-bound, only use "Today" for simple, short duration tasks.
156
+ -Ensure the SMART criteria is high-level, and based directly on the goal statement, and the user description.
157
+ - The **Specific** criteria should clarify what is to be achieved with the goal, and must directly reflect the goal statement, and must not imply any specific actions or processes.
158
+ - The **Measurable** criteria should define how you will know if the goal has been achieved. It should be a metric or some other way of validating that the goal is complete, and must not include implied actions or steps.
159
+ - The **Achievable** criteria should explain why the goal is achievable given the information provided by the user. It should specify any limitations or benefits.
160
+ - The **Relevant** criteria should specify why this goal is necessary, or what value it provides.
161
+ - The **Time-bound** criteria must specify when the goal must be achieved. For small tasks, this will be "Today". For larger tasks, the time-bound should be a general time estimate, and should not specify a specific date or time unless it has been specified by the user.
162
+ - A breakdown of dependencies and required resources for the project. Break down dependencies into actionable sub-tasks where applicable. Dependencies should be high-level, and not overly prescriptive, nor should they imply specific actions. Only include dependencies that are explicitly mentioned in the user description or directly implied from it. Do not include any specific timestamps, volumes, quantities or implied resources in the dependencies section, and do not include inferred actions.
163
+ - A clear identification of related goals and future applications.
164
+ - A detailed risk assessment with specific mitigation strategies. Focus on actionable items to mitigate the risks you have identified, ensuring they are tailored to the project's context.
165
+ - When identifying risks, consider common issues specific to the project's domain (e.g., construction delays, equipment failures, safety hazards, financial issues, security breaches, data losses). For each identified risk, generate a realistic and specific mitigation strategy that is actionable within the project's context. Try to extract risks based on user descriptions. Avoid being too specific, and avoid adding unrealistic risks and mitigation actions. Only include mitigation plans that are explicitly derived from the user description, or are implied from it.
166
+ - A comprehensive stakeholder analysis, identifying primary and secondary stakeholders, and outlining engagement strategies.
167
+ - **Primary Stakeholders:** Identify key roles or individuals directly responsible for executing the project. For small-scale or personal projects, this may simply be the person performing the task (e.g., "Coffee Brewer"). For large-scale projects, identify domain-specific roles (e.g., "Construction Manager," "Life Support Systems Engineer").
168
+ - **Secondary Stakeholders:** Identify external parties or collaborators relevant to the project. For small-scale projects, this may include suppliers or individuals indirectly affected by the project (e.g., "Coffee Supplier," "Household Members"). For large-scale projects, include regulatory bodies, material suppliers, or other external entities.
169
+ - When outlining engagement strategies for stakeholders, consider the nature of the project and their roles. Primary stakeholders should have regular updates and progress reports, and requests for information should be answered promptly. Secondary stakeholders may require updates on key milestones, reports for compliance, or timely notification of significant changes to project scope or timeline. For smaller projects, the engagement strategy and stakeholders can be omitted if they are not explicitly mentioned in the user description, or implied from it.
170
+ - **Note:** Do not assume the availability or involvement of any specific individuals beyond those directly mentioned in the user-provided project description. Generate all information independently from the provided description, and do not rely on any previous data or information from prior runs of this tool. Do not include any default information unless explicitly stated.
171
+ - A detailed overview of regulatory and compliance requirements, such as permits and licenses, and how compliance actions are planned.
172
+ - When considering regulatory and compliance requirements, identify any specific licenses or permits needed, and include compliance actions in the plan, such as "Apply for permit X", "Schedule compliance audit" and "Implement compliance plan for Y", and ensure compliance actions are included in the project timeline. For smaller projects, the regulatory compliance section can be omitted.
173
+ - Tags or keywords that allow users to easily find and categorize the project.
174
+ Adaptive Behavior:
175
+ - Automatically adjust the level of detail and formality based on the scale and complexity of the project. For small-scale or personal projects, keep the plan simple and avoid formal elements. For massive or complex projects, ensure plans include more formal elements, such as project charters or work breakdown structures, and provide detailed actions for project execution.
176
+ - Infer the appropriate stakeholders, risks, and resources based on the project's domain and context. Avoid overly formal or mismatched roles unless explicitly required by the project's context.
177
+ - For smaller tasks, only include resources that need to be purchased or otherwise explicitly acquired. Only include resources that are mentioned in the user description, or implied from it. Do not include personnel or stakeholders as a resource.
178
+ - Only include dependencies that are explicitly mentioned in the user description, or directly implied from it.
179
+ Prioritize feasibility, practicality, and alignment with the user-provided description. Ensure the plan is actionable, with concrete steps where possible and measurable outcomes.
180
+ When breaking down dependencies into sub-tasks, specify concrete actions (e.g., "Procure X", "Design Y", "Test Z"), and if possible, include resource requirements (e.g., "Procure 100 Units of X") and estimated timeframes where appropriate. However, for very small, simple tasks, the dependencies do not need a time element, and do not have to be overly specific.
181
+
182
+ Here's an example of the expected output format for a simple project:
183
+ {
184
+ "goal_statement": "Make a cup of coffee.",
185
+ "smart_criteria": {
186
+ "specific": "Prepare a cup of instant coffee, with milk and sugar if available.",
187
+ "measurable": "The completion of the task can be measured by the existence of a prepared cup of coffee.",
188
+ "achievable": "The task is achievable in the user's kitchen.",
189
+ "relevant": "The task will provide the user with a warm drink.",
190
+ "time_bound": "The task should be completed in 5 minutes."
191
+ },
192
+ "dependencies": [],
193
+ "resources_required": [ "instant coffee" ],
194
+ "related_goals": [ "satisfy hunger", "enjoy a drink" ],
195
+ "tags": [ "drink", "coffee", "simple" ]
196
+ }
197
+ """
198
+
199
+ CREATE_PROJECT_PLAN_SYSTEM_PROMPT = CREATE_PROJECT_PLAN_SYSTEM_PROMPT_3
200
+
201
+ T = TypeVar('T', bound=BaseModel)
202
+
203
+ @dataclass
204
+ class CreateProjectPlan:
205
+ """
206
+ Creating a project plan from a vague description.
207
+ """
208
+ system_prompt: str
209
+ user_prompt: str
210
+ response: dict
211
+ metadata: dict
212
+
213
+ @classmethod
214
+ def execute(cls, llm: LLM, user_prompt: str) -> 'CreateProjectPlan':
215
+ """
216
+ Invoke LLM to create project plan from a vague description.
217
+
218
+ :param llm: An instance of LLM.
219
+ :param user_prompt: A vague description of the project.
220
+ :return: An instance of CreateProjectPlan.
221
+ """
222
+ if not isinstance(llm, LLM):
223
+ raise ValueError("Invalid LLM instance.")
224
+ if not isinstance(user_prompt, str):
225
+ raise ValueError("Invalid user_prompt.")
226
+
227
+ system_prompt = CREATE_PROJECT_PLAN_SYSTEM_PROMPT.strip()
228
+ logger.debug(f"System Prompt:\n{system_prompt}")
229
+ logger.debug(f"User Prompt:\n{user_prompt}")
230
+
231
+ chat_message_list = [
232
+ ChatMessage(
233
+ role=MessageRole.SYSTEM,
234
+ content=system_prompt,
235
+ ),
236
+ ChatMessage(
237
+ role=MessageRole.USER,
238
+ content=user_prompt,
239
+ )
240
+ ]
241
+
242
+ sllm = llm.as_structured_llm(GoalDefinition)
243
+
244
+ logger.debug("Starting LLM chat interaction.")
245
+ start_time = time.perf_counter()
246
+ try:
247
+ chat_response = sllm.chat(chat_message_list)
248
+ except Exception as e:
249
+ logger.debug(f"LLM chat interaction failed: {e}")
250
+ logger.error("LLM chat interaction failed.", exc_info=True)
251
+ raise ValueError("LLM chat interaction failed.") from e
252
+
253
+ end_time = time.perf_counter()
254
+ duration = int(ceil(end_time - start_time))
255
+ response_byte_count = len(chat_response.message.content.encode('utf-8'))
256
+ logger.info(f"LLM chat interaction completed in {duration} seconds. Response byte count: {response_byte_count}")
257
+
258
+ json_response = chat_response.raw.model_dump()
259
+
260
+ metadata = dict(llm.metadata)
261
+ metadata["llm_classname"] = llm.class_name()
262
+ metadata["duration"] = duration
263
+ metadata["response_byte_count"] = response_byte_count
264
+
265
+ result = CreateProjectPlan(
266
+ system_prompt=system_prompt,
267
+ user_prompt=user_prompt,
268
+ response=json_response,
269
+ metadata=metadata
270
+ )
271
+ logger.debug("CreateProjectPlan instance created successfully.")
272
+ return result
273
+
274
+ def to_dict(self, include_metadata=True, include_user_prompt=True, include_system_prompt=True) -> dict:
275
+ d = self.response.copy()
276
+ if include_metadata:
277
+ d['metadata'] = self.metadata
278
+ if include_user_prompt:
279
+ d['user_prompt'] = self.user_prompt
280
+ if include_system_prompt:
281
+ d['system_prompt'] = self.system_prompt
282
+ return d
283
+
284
+ def save(self, file_path: str) -> None:
285
+ d = self.to_dict()
286
+ with open(file_path, 'w') as f:
287
+ f.write(json.dumps(d, indent=2))
288
+
289
+ if __name__ == "__main__":
290
+ import logging
291
+ from src.llm_factory import get_llm
292
+ from src.plan.find_plan_prompt import find_plan_prompt
293
+
294
+ logging.basicConfig(
295
+ level=logging.DEBUG,
296
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
297
+ handlers=[
298
+ logging.StreamHandler()
299
+ ]
300
+ )
301
+
302
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
303
+
304
+ llm = get_llm("ollama-llama3.1")
305
+ # llm = get_llm("openrouter-paid-gemini-2.0-flash-001")
306
+ # llm = get_llm("deepseek-chat")
307
+
308
+ print(f"Query:\n{plan_prompt}\n\n")
309
+ result = CreateProjectPlan.execute(llm, plan_prompt)
310
+ json_response = result.to_dict(include_system_prompt=False, include_user_prompt=False)
311
+ print(json.dumps(json_response, indent=2))
src/plan/create_wbs_level1.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WBS Level 1: Create a Work Breakdown Structure (WBS) from a project plan.
3
+
4
+ https://en.wikipedia.org/wiki/Work_breakdown_structure
5
+ """
6
+ import os
7
+ import json
8
+ import time
9
+ from math import ceil
10
+ from typing import List, Optional
11
+ from uuid import uuid4
12
+ from dataclasses import dataclass
13
+ from pydantic import BaseModel, Field
14
+ from llama_index.core.llms.llm import LLM
15
+
16
+ class WBSLevel1(BaseModel):
17
+ """
18
+ Represents the top-level details of a Work Breakdown Structure (WBS)
19
+ """
20
+ project_title: str = Field(
21
+ description="A clear, overarching title that conveys the primary objective of the project. Serves as the projects strategic anchor, guiding all subsequent tasks and deliverables."
22
+ )
23
+ final_deliverable: str = Field(
24
+ description="A detailed description of the projects ultimate outcome or product upon completion. Clearly states the final state or result that the team aims to achieve."
25
+ )
26
+
27
+ QUERY_PREAMBLE = f"""
28
+ The task here:
29
+ Create a work breakdown structure level 1 for this project.
30
+
31
+ Focus on providing the following:
32
+ - 'project_title': A 1- to 3-word name, extremely concise.
33
+ - 'final_deliverable': A 1- to 3-word result, extremely concise.
34
+
35
+ The project plan:
36
+ """
37
+
38
+
39
+ @dataclass
40
+ class CreateWBSLevel1:
41
+ """
42
+ WBS Level 1: Creating a Work Breakdown Structure (WBS) from a project plan.
43
+ """
44
+ query: str
45
+ response: dict
46
+ metadata: dict
47
+ id: str
48
+ project_title: str
49
+ final_deliverable: str
50
+
51
+ @classmethod
52
+ def execute(cls, llm: LLM, query: str) -> 'CreateWBSLevel1':
53
+ """
54
+ Invoke LLM to create a Work Breakdown Structure (WBS) from a json representation of a project plan.
55
+ """
56
+ if not isinstance(llm, LLM):
57
+ raise ValueError("Invalid LLM instance.")
58
+ if not isinstance(query, str):
59
+ raise ValueError("Invalid query.")
60
+
61
+ start_time = time.perf_counter()
62
+
63
+ sllm = llm.as_structured_llm(WBSLevel1)
64
+ response = sllm.complete(QUERY_PREAMBLE + query)
65
+ json_response = json.loads(response.text)
66
+
67
+ end_time = time.perf_counter()
68
+ duration = int(ceil(end_time - start_time))
69
+
70
+ metadata = dict(llm.metadata)
71
+ metadata["llm_classname"] = llm.class_name()
72
+ metadata["duration"] = duration
73
+ metadata["query"] = query
74
+
75
+ project_id = str(uuid4())
76
+ result = CreateWBSLevel1(
77
+ query=query,
78
+ response=json_response,
79
+ metadata=metadata,
80
+ id=project_id,
81
+ project_title=json_response['project_title'],
82
+ final_deliverable=json_response['final_deliverable']
83
+ )
84
+ return result
85
+
86
+ def raw_response_dict(self, include_metadata=True) -> dict:
87
+ d = self.response.copy()
88
+ if include_metadata:
89
+ d['metadata'] = self.metadata
90
+ return d
91
+
92
+ def cleanedup_dict(self) -> dict:
93
+ return {
94
+ "id": self.id,
95
+ "project_title": self.project_title,
96
+ "final_deliverable": self.final_deliverable
97
+ }
98
+
99
+ if __name__ == "__main__":
100
+ from llama_index.llms.ollama import Ollama
101
+ # TODO: Eliminate hardcoded paths
102
+ path = '/Users/neoneye/Desktop/planexe_data/plan.json'
103
+
104
+ print(f"file: {path}")
105
+ with open(path, 'r', encoding='utf-8') as f:
106
+ plan_json = json.load(f)
107
+
108
+ if 'metadata' in plan_json:
109
+ del plan_json['metadata']
110
+
111
+ model_name = "llama3.1:latest"
112
+ # model_name = "qwen2.5-coder:latest"
113
+ # model_name = "phi4:latest"
114
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
115
+
116
+ query = json.dumps(plan_json, indent=2)
117
+ print(f"\nQuery: {query}")
118
+ result = CreateWBSLevel1.execute(llm, query)
119
+ print("\n\nResult:")
120
+ print(json.dumps(result.raw_response_dict(), indent=2))
src/plan/create_wbs_level2.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WBS Level 2: Create a Work Breakdown Structure (WBS) from a project plan.
3
+
4
+ https://en.wikipedia.org/wiki/Work_breakdown_structure
5
+
6
+ Focus is on the "Process style".
7
+ Focus is not on the "product style".
8
+ """
9
+ import os
10
+ import json
11
+ import time
12
+ from math import ceil
13
+ from typing import List, Optional
14
+ from uuid import uuid4
15
+ from dataclasses import dataclass
16
+ from pydantic import BaseModel, Field
17
+ from llama_index.core.llms.llm import LLM
18
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
19
+
20
+ class SubtaskDetails(BaseModel):
21
+ subtask_wbs_number: str = Field(
22
+ description="The unique identifier assigned to each subtask. Example: ['1.', '2.', '3.', '6.2.2', '6.2.3', '6.2.4', 'Subtask 5:', 'Subtask 6:', 'S3.', 'S4.']."
23
+ )
24
+ subtask_title: str = Field(
25
+ description="Start with a verb to clearly indicate the action required. Example: ['Secure funding', 'Obtain construction permits', 'Electrical installation', 'Commissioning and handover']."
26
+ )
27
+
28
+ class MajorPhaseDetails(BaseModel):
29
+ """
30
+ A major phase in the project decomposed into smaller tasks.
31
+ """
32
+ major_phase_wbs_number: str = Field(
33
+ description="The unique identifier assigned to each major phase. Example: ['1.', '2.', '3.', 'Phase 1:', 'Phase 2:', 'P1.', 'P2.']."
34
+ )
35
+ major_phase_title: str = Field(
36
+ description="Action-oriented title of this primary phase of the project. Example: ['Project Initiation', 'Procurement', 'Construction', 'Operation and Maintenance']."
37
+ )
38
+ subtasks: list[SubtaskDetails] = Field(
39
+ description="List of the subtasks or activities."
40
+ )
41
+
42
+ class WorkBreakdownStructure(BaseModel):
43
+ """
44
+ The Work Breakdown Structure (WBS) is a hierarchical decomposition of the total scope of work to accomplish project objectives.
45
+ It organizes the project into smaller, more manageable components.
46
+ """
47
+ major_phase_details: list[MajorPhaseDetails] = Field(
48
+ description="List with each major phase broken down into subtasks or activities."
49
+ )
50
+
51
+ QUERY_PREAMBLE = f"""
52
+ Create a work breakdown structure level 2 for this project.
53
+
54
+ A task can always be broken down into smaller, more manageable subtasks.
55
+
56
+ """
57
+
58
+ @dataclass
59
+ class CreateWBSLevel2:
60
+ """
61
+ WBS Level 2: Creating a Work Breakdown Structure (WBS) from a project plan.
62
+ """
63
+ query: str
64
+ response: dict
65
+ metadata: dict
66
+ major_phases_with_subtasks: list[dict]
67
+ major_phases_uuids: list[str]
68
+ task_uuids: list[str]
69
+
70
+ @classmethod
71
+ def format_query(cls, plan_json: dict, wbs_level1_json: dict) -> str:
72
+ """
73
+ Format the query for creating a Work Breakdown Structure (WBS) level 2.
74
+ """
75
+ if not isinstance(plan_json, dict):
76
+ raise ValueError("Invalid plan_json.")
77
+ if not isinstance(wbs_level1_json, dict):
78
+ raise ValueError("Invalid wbs_level1_json.")
79
+
80
+ # Having a uuid in the WBS Level 1 data trend to confuse the LLM, causing the LLM to attempt to insert all kinds of ids in the response.
81
+ # Removing the id from the WBS Level 1 data, and there is less confusion about what the LLM should do.
82
+ wbs_level1_json_without_id = wbs_level1_json.copy()
83
+ wbs_level1_json_without_id.pop("id", None)
84
+
85
+ query = f"""
86
+ The project plan:
87
+ {format_json_for_use_in_query(plan_json)}
88
+
89
+ WBS Level 1:
90
+ {format_json_for_use_in_query(wbs_level1_json_without_id)}
91
+ """
92
+ return query
93
+
94
+ @classmethod
95
+ def execute(cls, llm: LLM, query: str) -> 'CreateWBSLevel2':
96
+ """
97
+ Invoke LLM to create a Work Breakdown Structure (WBS) from a json representation of a project plan.
98
+ """
99
+ if not isinstance(llm, LLM):
100
+ raise ValueError("Invalid LLM instance.")
101
+ if not isinstance(query, str):
102
+ raise ValueError("Invalid query.")
103
+
104
+ start_time = time.perf_counter()
105
+
106
+ sllm = llm.as_structured_llm(WorkBreakdownStructure)
107
+ response = sllm.complete(QUERY_PREAMBLE + query)
108
+ json_response = json.loads(response.text)
109
+
110
+ end_time = time.perf_counter()
111
+ duration = int(ceil(end_time - start_time))
112
+
113
+ metadata = dict(llm.metadata)
114
+ metadata["llm_classname"] = llm.class_name()
115
+ metadata["duration"] = duration
116
+
117
+ # Cleanup the json response from the LLM model, assign unique ids to each activity.
118
+ result_major_phases_with_subtasks = []
119
+ result_major_phases_uuids = []
120
+ result_task_uuids = []
121
+ for major_phase_detail in json_response['major_phase_details']:
122
+ subtask_list = []
123
+ for subtask in major_phase_detail['subtasks']:
124
+ subtask_title = subtask['subtask_title']
125
+ uuid = str(uuid4())
126
+ subtask_item = {
127
+ "id": uuid,
128
+ "description": subtask_title,
129
+ }
130
+ subtask_list.append(subtask_item)
131
+ result_task_uuids.append(uuid)
132
+
133
+ uuid = str(uuid4())
134
+ major_phase_item = {
135
+ "id": uuid,
136
+ "major_phase_title": major_phase_detail['major_phase_title'],
137
+ "subtasks": subtask_list,
138
+ }
139
+ result_major_phases_with_subtasks.append(major_phase_item)
140
+ result_major_phases_uuids.append(uuid)
141
+
142
+ result = CreateWBSLevel2(
143
+ query=query,
144
+ response=json_response,
145
+ metadata=metadata,
146
+ major_phases_with_subtasks=result_major_phases_with_subtasks,
147
+ major_phases_uuids=result_major_phases_uuids,
148
+ task_uuids=result_task_uuids
149
+ )
150
+ return result
151
+
152
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
153
+ d = self.response.copy()
154
+ if include_metadata:
155
+ d['metadata'] = self.metadata
156
+ if include_query:
157
+ d['query'] = self.query
158
+ return d
159
+
160
+ if __name__ == "__main__":
161
+ from llama_index.llms.ollama import Ollama
162
+
163
+ # TODO: Eliminate hardcoded paths
164
+ path = '/Users/neoneye/Desktop/planexe_data/plan.json'
165
+
166
+ wbs_level1_json = {
167
+ "id": "d0169227-bf29-4a54-a898-67d6ff4d1193",
168
+ "project_title": "Establish a solar farm in Denmark",
169
+ "final_deliverable": "Solar farm operational",
170
+ }
171
+
172
+ print(f"file: {path}")
173
+ with open(path, 'r', encoding='utf-8') as f:
174
+ plan_json = json.load(f)
175
+
176
+ query = CreateWBSLevel2.format_query(plan_json, wbs_level1_json)
177
+
178
+ model_name = "llama3.1:latest"
179
+ # model_name = "qwen2.5-coder:latest"
180
+ # model_name = "phi4:latest"
181
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
182
+
183
+ print(f"Query: {query}")
184
+ result = CreateWBSLevel2.execute(llm, query)
185
+
186
+ print("Response:")
187
+ response_dict = result.raw_response_dict(include_query=False)
188
+ print(json.dumps(response_dict, indent=2))
189
+
190
+ print("\n\nExtracted result:")
191
+ print(json.dumps(result.major_phases_with_subtasks, indent=2))
192
+
src/plan/create_wbs_level3.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WBS Level 3: Create a Work Breakdown Structure (WBS) from a project plan.
3
+
4
+ https://en.wikipedia.org/wiki/Work_breakdown_structure
5
+
6
+ The "progressive elaboration" technique is used to decompose big tasks into smaller, more manageable subtasks.
7
+ """
8
+ import os
9
+ import json
10
+ import time
11
+ from dataclasses import dataclass
12
+ from math import ceil
13
+ from typing import List, Optional
14
+ from uuid import uuid4
15
+ from dataclasses import dataclass
16
+ from pydantic import BaseModel, Field
17
+ from llama_index.core.llms.llm import LLM
18
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
19
+
20
+ class WBSSubtask(BaseModel):
21
+ """
22
+ A subtask.
23
+ """
24
+ name: str = Field(
25
+ description="Short name of the subtask, such as: Prepare necessary documentation for permits, Conduct interviews with potential contractors, Secure final approval of funds."
26
+ )
27
+ description: str = Field(
28
+ description="Longer text that describes the subtask in more detail, such as: Objective, Scope, Steps, Deliverables."
29
+ )
30
+ resources_needed: list[str] = Field(
31
+ description="List of resources needed to complete the subtask. Example: ['Project manager', 'Architect', 'Engineer', 'Construction crew']."
32
+ )
33
+
34
+ class WBSTaskDetails(BaseModel):
35
+ """
36
+ A big task in the project decomposed into a few smaller tasks.
37
+ """
38
+ subtasks: list[WBSSubtask] = Field(
39
+ description="List of subtasks."
40
+ )
41
+
42
+ QUERY_PREAMBLE = f"""
43
+ Decompose a big task into smaller, more manageable subtasks.
44
+
45
+ Split the task into 3 to 5 subtasks.
46
+
47
+ Pick a subtask name that is short, max 7 words or max 60 characters.
48
+
49
+ Don't enumerate the subtasks with integers or letters.
50
+
51
+ Don't assign a uuid to the subtask.
52
+
53
+ """
54
+
55
+ @dataclass
56
+ class CreateWBSLevel3:
57
+ """
58
+ WBS Level 3: Creating a Work Breakdown Structure (WBS) from a project plan.
59
+ """
60
+ query: str
61
+ response: dict
62
+ metadata: dict
63
+ tasks: list[dict]
64
+ task_uuids: list[str]
65
+
66
+ @classmethod
67
+ def format_query(cls, plan_json: dict, wbs_level1_json: dict, wbs_level2_json: list, wbs_level2_task_durations_json: dict, decompose_task_id: str) -> str:
68
+ if not isinstance(plan_json, dict):
69
+ raise ValueError("Invalid plan_json.")
70
+ if not isinstance(wbs_level1_json, dict):
71
+ raise ValueError("Invalid wbs_level1_json.")
72
+ if not isinstance(wbs_level2_json, list):
73
+ raise ValueError("Invalid wbs_level1_json.")
74
+ if not isinstance(wbs_level2_task_durations_json, list):
75
+ raise ValueError("Invalid wbs_level2_task_durations_json.")
76
+ if not isinstance(decompose_task_id, str):
77
+ raise ValueError("Invalid decompose_task_id.")
78
+
79
+ query = f"""
80
+ The project plan:
81
+ {format_json_for_use_in_query(plan_json)}
82
+
83
+ WBS Level 1:
84
+ {format_json_for_use_in_query(wbs_level1_json)}
85
+
86
+ WBS Level 2:
87
+ {format_json_for_use_in_query(wbs_level2_json)}
88
+
89
+ WBS Level 2 time estimates:
90
+ {format_json_for_use_in_query(wbs_level2_task_durations_json)}
91
+
92
+ Only decompose this task:
93
+ "{decompose_task_id}"
94
+ """
95
+ return query
96
+
97
+ @classmethod
98
+ def execute(cls, llm: LLM, query: str, decompose_task_id: str) -> 'CreateWBSLevel3':
99
+ """
100
+ Invoke LLM to decompose a big WBS level 2 task into smaller tasks.
101
+ """
102
+ if not isinstance(llm, LLM):
103
+ raise ValueError("Invalid LLM instance.")
104
+ if not isinstance(query, str):
105
+ raise ValueError("Invalid query.")
106
+ if not isinstance(decompose_task_id, str):
107
+ raise ValueError("Invalid decompose_task_id.")
108
+
109
+ start_time = time.perf_counter()
110
+
111
+ sllm = llm.as_structured_llm(WBSTaskDetails)
112
+ response = sllm.complete(QUERY_PREAMBLE + query)
113
+ json_response = json.loads(response.text)
114
+
115
+ end_time = time.perf_counter()
116
+ duration = int(ceil(end_time - start_time))
117
+
118
+ metadata = dict(llm.metadata)
119
+ metadata["llm_classname"] = llm.class_name()
120
+ metadata["duration"] = duration
121
+
122
+ # Cleanup the json response from the LLM model, assign unique ids to each subtask.
123
+ parent_task_id = decompose_task_id
124
+ result_tasks = []
125
+ result_task_uuids = []
126
+ for subtask in json_response['subtasks']:
127
+ name = subtask['name']
128
+ description = subtask['description']
129
+ resources_needed = subtask['resources_needed']
130
+ uuid = str(uuid4())
131
+ subtask_item = {
132
+ "id": uuid,
133
+ "name": name,
134
+ "description": description,
135
+ "resources_needed": resources_needed,
136
+ "parent_id": parent_task_id
137
+ }
138
+ result_tasks.append(subtask_item)
139
+ result_task_uuids.append(uuid)
140
+
141
+ result = CreateWBSLevel3(
142
+ query=query,
143
+ response=json_response,
144
+ metadata=metadata,
145
+ tasks=result_tasks,
146
+ task_uuids=result_task_uuids
147
+ )
148
+ return result
149
+
150
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
151
+ d = self.response.copy()
152
+ if include_metadata:
153
+ d['metadata'] = self.metadata
154
+ if include_query:
155
+ d['query'] = self.query
156
+ return d
157
+
158
+ if __name__ == "__main__":
159
+ from llama_index.llms.ollama import Ollama
160
+
161
+ # TODO: Eliminate hardcoded paths
162
+ basepath = '/Users/neoneye/Desktop/planexe_data'
163
+
164
+ def load_json(relative_path: str) -> dict:
165
+ path = os.path.join(basepath, relative_path)
166
+ print(f"loading file: {path}")
167
+ with open(path, 'r', encoding='utf-8') as f:
168
+ the_json = json.load(f)
169
+ return the_json
170
+
171
+ plan_json = load_json('002-project_plan.json')
172
+ wbs_level1_json = load_json('006-wbs_level1.json')
173
+ wbs_level2_json = load_json('008-wbs_level2.json')
174
+ wbs_level2_task_durations_json = load_json('012-task_durations.json')
175
+ decompose_task_id = "1c690f4a-ae8e-493d-9e47-6da58ef5b24c"
176
+
177
+ query = CreateWBSLevel3.format_query(plan_json, wbs_level1_json, wbs_level2_json, wbs_level2_task_durations_json, decompose_task_id)
178
+
179
+ model_name = "llama3.1:latest"
180
+ # model_name = "qwen2.5-coder:latest"
181
+ # model_name = "phi4:latest"
182
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
183
+
184
+ print(f"Query: {query}")
185
+ result = CreateWBSLevel3.execute(llm, query, decompose_task_id)
186
+
187
+ print("\n\nResponse:")
188
+ print(json.dumps(result.raw_response_dict(include_query=False), indent=2))
189
+
190
+ print("\n\nExtracted tasks:")
191
+ print(json.dumps(result.tasks, indent=2))
192
+
src/plan/data/simple_plan_prompts.jsonl ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"id": "fdbac6bc-6853-47f3-b7ec-bc0051314952", "prompt": "I'm envisioning a streamlined global language—free of archaic features like gendered terms and excessive suffixes, taking cues from LLM tokenization. Some regions might only choose to adopt certain parts of this modern language. Would humanity ultimately benefit more from preserving many distinct languages, or uniting around a single, optimized one?", "tags": ["language", "tokenization"]}
2
+ {"id": "762b64e2-5ac8-4684-807a-efd3e81d6bc1", "prompt": "Create a detailed report examining the current situation of microplastics within the world's oceans.", "tags": ["ocean", "microplastics", "climate change", "sustainability"]}
3
+ {"id": "930c2abc-faa7-4c21-8ae1-f0323cbcd120", "prompt": "Open the first space elevator terminal in Berlin, Germany, connecting Earths surface to orbit.", "tags": ["space", "exploration", "berlin", "germany"]}
4
+ {"id": "45763178-8ba8-4a86-adcd-63ed19d4d47b", "prompt": "Establish a humanoid robot factory in Paris, France.", "tags": ["paris", "france", "robots"]}
5
+ {"id": "d70ced0b-d5c7-4b84-88d7-18a5ada2cfee", "prompt": "Construct a new metro line under the city center of Copenhagen, Denmark.", "tags": ["denmark", "copenhagen", "metro"]}
6
+ {"id": "f24a6ba9-20ce-40bb-866a-263b87b5ddcc", "prompt": "I want to make a restaurant for puzzle solving happy people. An important part is that humans are solving puzzles with each other. While having something to drink and eat.", "tags": ["restaurant", "puzzle", "food"]}
7
+ {"id": "da8da7a6-954c-4f88-91c9-53f98a934868", "prompt": "I want a cup of coffee. I have instant coffee and a cup and water. I don't use sugar, milk. I have done it many times before.", "tags": ["small task", "trivial", "coffee"]}
8
+ {"id": "9d008681-8710-431c-bae4-0fc27ce3cc36", "prompt": "I want to find the TV remote. It's probably in the fridge again. I had it last night when I watched TV.", "tags": ["small task", "trivial", "lost item"]}
9
+ {"id": "3deda46b-9c9d-4078-a72c-15299b70d915", "prompt": "I need to water my houseplants. I have a watering can and access to water. The plants are in the living room and bedroom. There are 5 plants in total. They need to be watered once a week.", "tags": ["small task", "trivial", "plants"]}
10
+ {"id": "0a61aae5-472d-4e63-8a4e-cf976cb5064b", "prompt": "I need to set an alarm for tomorrow morning. I have a smartphone and know the time I want to wake up.", "tags": ["small task", "trivial", "sleep"]}
11
+ {"id": "2eaa697a-0657-4de2-aadc-a6f314e88e98", "prompt": "I need to take out the trash. All the bins is in the kitchen, and the dumpsters are outside, such as: metal, plastics, bio. Where I live, citizens must sort the trash into the correct bins.", "tags": ["small task", "trivial", "trash"]}
12
+ {"id": "9040f467-cce5-4e68-8686-48d4464c4d02", "prompt": "I want to go on a 14 days vacation trip to Paris. Book a cheap flight. Identify a cheap hotel. I want to visit the Eiffel tower. What other tourist attractions are a must.", "tags": ["travel", "paris", "eiffel tower", "hotel"]}
13
+ {"id": "faca8cec-28bb-4499-b9e9-1efc00c4aa2b", "prompt": "Construct a pyramid of modern day, year 2025, built following the same principles as the old pyramids (2000 BC), no use of modern tools.", "tags": ["historical", "replica", "pyramid"]}
14
+ {"id": "98a8c63e-4770-4ee1-aef8-693800deec0e", "prompt": "Construct a replica of Stonehenge using only the tools and methods available in 2500 BC, at a site in rural England.", "tags": ["historical", "replica", "stonehenge"]}
15
+ {"id": "39bc819c-ee86-44c8-b1d4-d6bf3117cb0e", "prompt": "Create a fully functional, medieval-style castle using only materials and techniques from the 12th century, located in Scotland.", "tags": ["historical", "replica", "castle"]}
16
+ {"id": "6860b2ae-39f0-4517-b827-95befbf142ac", "prompt": "Manned moon mission, for establishing a permanent base.", "tags": ["moon", "space", "exploration"]}
17
+ {"id": "b37da5bf-1e67-43b6-8c76-8d735f89ef6b", "prompt": "Construct a massive, multi-level underground complex designed to sustain thousands of people indefinitely. This silo would feature self-contained ecosystems, including residential areas, agricultural zones, and industrial facilities spread across 144 floors. The structure would maintain stringent rules to keep order and control information about the outside world, believed to be toxic. Advanced surveillance and security systems would enforce these rules. The silo would be a complete, self-sustaining society, with its own power generation, water recycling, and air filtration systems, embodying a dystopian, controlled environment. Funding for this project comes from a mix of government allocations and private investments from elite stakeholders.", "tags": ["silo", "exploration", "dystopian"]}
18
+ {"id": "c2c45867-be60-4690-aac1-530627fc0818", "prompt": "Deep cave exploration to find new lifeforms in extreme conditions, in the newly found highly radiative cave near Copenhagen, Denmark.", "tags": ["denmark", "copenhagen", "cave", "radioactive"]}
19
+ {"id": "d3e10877-446f-4eb0-8027-864e923973b0", "prompt": "Construct a train bridge between Denmark and England.", "tags": ["denmark", "england", "bridge"]}
20
+ {"id": "9fbb7ff9-5dc3-44f4-9823-dba3f31d3661", "prompt": "Write a Python script for a bouncing yellow ball within a square. Make sure to handle collision detection. Make the square slightly rotate. Implement it in Python. Make sure the ball stays within the square.", "tags": ["programming", "python", "collision detection"]}
21
+ {"id": "676cbca8-5d49-42a0-8826-398318004703", "prompt": "Write a Python script for a snake shape keep bouncing within a pentagon. Make sure to handle collision detection properly. Make the pentagon slowly rotate.", "tags": ["programming", "python", "collision detection"]}
22
+ {"id": "a9113924-6148-4a0c-b72a-eecdb856e1e2", "prompt": "Investigate outbreak of a deadly new disease in the jungle.", "tags": ["outbreak", "jungle"]}
23
+ {"id": "4dc34d55-0d0d-4e9d-92f4-23765f49dd29", "prompt": "Establish a solar farm in Denmark.", "tags": ["denmark", "energy", "sun"]}
src/plan/estimate_wbs_task_durations.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ https://en.wikipedia.org/wiki/Work_breakdown_structure
3
+ """
4
+ import os
5
+ import json
6
+ import time
7
+ from math import ceil
8
+ from typing import List, Optional
9
+ from uuid import uuid4
10
+ from dataclasses import dataclass
11
+ from pydantic import BaseModel, Field
12
+ from llama_index.core.llms.llm import LLM
13
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
14
+
15
+ class TaskTimeEstimateDetail(BaseModel):
16
+ """
17
+ Details about a task duration, lower/upper bounds. Potential risks impacting the duration.
18
+ """
19
+ task_id: str = Field(
20
+ description="UUID that uniquely identifies the task."
21
+ )
22
+ delay_risks: str = Field(
23
+ description="Possible issues that may delay the task. Example: ['Weather-related disruptions', 'Third-party vendors might fail to deliver on time', 'Key team members might be unavailable']. **This field MUST be filled with a meaningful description. Do not leave it empty.**"
24
+ )
25
+ mitigation_strategy: str = Field(
26
+ description="Actions or strategies to minimize the risk of delays. Example: ['Engage backup vendors', 'Schedule regular progress reviews', 'Establish clear communication channels']. **This field MUST be filled with a meaningful and specific strategy. Do not leave it empty.**"
27
+ )
28
+ days_min: int = Field(
29
+ description="Number of days, the best case scenario. If not applicable use minus 1."
30
+ )
31
+ days_max: int = Field(
32
+ description="Number of days, the worst case scenario. If not applicable use minus 1."
33
+ )
34
+ days_realistic: int = Field(
35
+ description="Number of days, in the realistic scenario. If not applicable use minus 1."
36
+ )
37
+
38
+ class TimeEstimates(BaseModel):
39
+ """
40
+ Estimating realistic durations for each task and appropriately assigning resources
41
+ ensures that the project stays on schedule and within budget.
42
+ """
43
+ task_details: list[TaskTimeEstimateDetail] = Field(
44
+ description="List with tasks with time estimates."
45
+ )
46
+
47
+ QUERY_PREAMBLE = f"""
48
+ Assign estimated durations for each task and subtask.
49
+ Ensure a consistent voice and phrasing across tasks.
50
+
51
+ **For each task, you MUST provide a meaningful description for both 'delay_risks' and 'mitigation_strategy'. Do not leave these fields as empty strings.**
52
+
53
+ **Example of good 'delay_risks' and 'mitigation_strategy':**
54
+ For the task of "Define project scope and objectives":
55
+ - delay_risks: "Lack of clear initial requirements from stakeholders, potential for scope creep later in the project."
56
+ - mitigation_strategy: "Conduct thorough initial meetings with all key stakeholders to gather requirements, establish a clear change management process."
57
+
58
+ """
59
+
60
+ @dataclass
61
+ class EstimateWBSTaskDurations:
62
+ """
63
+ Enrich an existing Work Breakdown Structure (WBS) with task duration estimates.
64
+ """
65
+ query: str
66
+ response: dict
67
+ metadata: dict
68
+
69
+ @classmethod
70
+ def format_query(cls, plan_json: dict, wbs_level2_json: list, task_ids: list[str]) -> str:
71
+ if not isinstance(plan_json, dict):
72
+ raise ValueError("Invalid plan_json.")
73
+ if not isinstance(wbs_level2_json, list):
74
+ raise ValueError("Invalid wbs_level1_json.")
75
+ if not isinstance(task_ids, list):
76
+ raise ValueError("Invalid task_ids.")
77
+
78
+ """
79
+ Wrap the task ids in quotes, so it looks like this:
80
+ "0ca58751-3abd-44d0-b24b-ebcf14c794e7"
81
+ "86f0ed30-ba23-46e4-83d9-ef53d95ff054"
82
+ "58d5dcc3-7385-4919-adc1-e1f84727e9d2"
83
+ """
84
+ task_ids_in_quotes = [f'"{task_id}"' for task_id in task_ids]
85
+ task_id_strings = "\n".join(task_ids_in_quotes)
86
+
87
+ query = f"""
88
+ The project plan:
89
+ {format_json_for_use_in_query(plan_json)}
90
+
91
+ The Work Breakdown Structure (WBS):
92
+ {format_json_for_use_in_query(wbs_level2_json)}
93
+
94
+ Only estimate these {len(task_ids)} tasks:
95
+ {task_id_strings}
96
+ """
97
+ return query
98
+
99
+ @classmethod
100
+ def execute(cls, llm: LLM, query: str) -> 'EstimateWBSTaskDurations':
101
+ """
102
+ Invoke LLM to estimate task durations from a json representation of a project plan and Work Breakdown Structure (WBS).
103
+
104
+ Executing with too many task_ids may result in a timeout, where the LLM cannot complete the task within a reasonable time.
105
+ Split the task_ids into smaller chunks of around 3 task_ids each, and process them one at a time.
106
+ """
107
+ if not isinstance(llm, LLM):
108
+ raise ValueError("Invalid LLM instance.")
109
+ if not isinstance(query, str):
110
+ raise ValueError("Invalid query.")
111
+
112
+ start_time = time.perf_counter()
113
+
114
+ sllm = llm.as_structured_llm(TimeEstimates)
115
+ response = sllm.complete(QUERY_PREAMBLE + query)
116
+ json_response = json.loads(response.text)
117
+
118
+ end_time = time.perf_counter()
119
+ duration = int(ceil(end_time - start_time))
120
+
121
+ metadata = dict(llm.metadata)
122
+ metadata["llm_classname"] = llm.class_name()
123
+ metadata["duration"] = duration
124
+
125
+ result = EstimateWBSTaskDurations(
126
+ query=query,
127
+ response=json_response,
128
+ metadata=metadata,
129
+ )
130
+ return result
131
+
132
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
133
+ d = self.response.copy()
134
+ if include_metadata:
135
+ d['metadata'] = self.metadata
136
+ if include_query:
137
+ d['query'] = self.query
138
+ return d
139
+
140
+ if __name__ == "__main__":
141
+ from llama_index.llms.ollama import Ollama
142
+
143
+ # TODO: Eliminate hardcoded paths
144
+ basepath = '/Users/neoneye/Desktop/planexe_data'
145
+
146
+ def load_json(relative_path: str) -> dict:
147
+ path = os.path.join(basepath, relative_path)
148
+ print(f"loading file: {path}")
149
+ with open(path, 'r', encoding='utf-8') as f:
150
+ the_json = json.load(f)
151
+ return the_json
152
+
153
+ plan_json = load_json('002-project_plan.json')
154
+ wbs_level2_json = load_json('006-wbs_level2.json')
155
+
156
+ task_ids = [
157
+ "c6a249af-b8d3-4d4c-b3ef-8a5caa8793d4",
158
+ "622fa6f1-6252-445e-8b5a-2a5c75683a80",
159
+ "fdaa706e-3d3b-4166-9730-7ea3e238d0cf"
160
+ ]
161
+
162
+ query = EstimateWBSTaskDurations.format_query(plan_json, wbs_level2_json, task_ids)
163
+
164
+ model_name = "llama3.1:latest"
165
+ # model_name = "qwen2.5-coder:latest"
166
+ # model_name = "phi4:latest"
167
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
168
+
169
+ print(f"Query: {query}")
170
+ result = EstimateWBSTaskDurations.execute(llm, query)
171
+
172
+ print("\n\nResponse:")
173
+ response_dict = result.raw_response_dict(include_query=False)
174
+ print(json.dumps(response_dict, indent=2))
src/plan/expert_cost.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Ask a specific expert about estimating cost.
3
+ """
4
+ import json
5
+ import time
6
+ from math import ceil
7
+ from typing import Optional
8
+ from enum import Enum
9
+ from dataclasses import dataclass
10
+ from pydantic import BaseModel, Field
11
+ from llama_index.core.llms import ChatMessage, MessageRole
12
+ from llama_index.core.llms.llm import LLM
13
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
14
+
15
+ class CostUnit(str, Enum):
16
+ # An hour is 60 minutes.
17
+ hour = 'hour'
18
+
19
+ # A day is 24 hours.
20
+ day = 'day'
21
+
22
+ # A single upfront fee that covers the entire cost of a project.
23
+ lumpsum = 'lumpsum'
24
+
25
+ # A single discrete unit or piece of equipment.
26
+ item = 'item'
27
+
28
+ # When no other enum value is applicable.
29
+ other = 'other'
30
+
31
+ class CostComponent(BaseModel):
32
+ name: str = Field(description="Human-readable name of the cost component.")
33
+ unit: CostUnit = Field(description="Indicates how costs are measured.")
34
+ quantity: float = Field(description="Number of units, if applicable.")
35
+ currency: str = Field(description="What currency used in this cost component, such as: USD, EUR.")
36
+ unit_cost: float = Field(description="Cost per unit, if applicable.")
37
+ labor_cost: float = Field(description="Cost related to labor.")
38
+ material_cost: float = Field(description="Cost related to materials.")
39
+ equipment_cost: float = Field(description="Cost related to equipment.")
40
+ overhead_cost: float = Field(description="Indirect or overhead costs.")
41
+ contingency_rate: float = Field(description="Higher contingency rates for riskier tasks.")
42
+
43
+ class CostEstimateItem(BaseModel):
44
+ task_id: str = Field(description="Unique identifier for the task.")
45
+ task_name: str = Field(description="Name of the task.")
46
+ cost_component_list: list[CostComponent] = Field(description="Multiple cost components.")
47
+ min_cost: int = Field(description="Minimum estimated cost.")
48
+ max_cost: int = Field(description="Maximum estimated cost.")
49
+ realistic_cost: int = Field(description="Most likely cost estimate.")
50
+ assumptions: list[str] = Field(description="Assumptions made during estimation.")
51
+ high_risks: list[str] = Field(description="Potential risks affecting cost. High risk level.")
52
+ medium_risks: list[str] = Field(description="Potential risks affecting cost. Medium risk level.")
53
+ low_risks: list[str] = Field(description="Potential risks affecting cost. Low risk level.")
54
+ dependencies_impact: str = Field(description="Impact of task dependencies on cost.")
55
+
56
+ class ExpertCostEstimationResponse(BaseModel):
57
+ cost_estimates: list[CostEstimateItem] = Field(description="List of cost estimates for tasks.")
58
+ primary_actions: list[str] = Field(description="Actionable steps to refine cost estimates.")
59
+ secondary_actions: list[str] = Field(description="Additional suggestions for cost management.")
60
+ follow_up_consultation: str = Field(description="Topics for the next consultation.")
61
+
62
+ @dataclass
63
+ class Document:
64
+ name: str
65
+ content: str
66
+
67
+ QUERY_PREAMBLE = f"""
68
+ Provide detailed and accurate cost estimates for the provided tasks.
69
+
70
+ Use the following guidelines:
71
+ - Provide minimum, maximum, and realistic cost estimates.
72
+ - Break down costs into components such as labor, materials, equipment, subcontractors, overhead, and miscellaneous.
73
+ - State any assumptions made during estimation.
74
+ - Highlight potential risks that could affect costs.
75
+ - Explain how task dependencies impact the cost.
76
+
77
+ Ensure that your estimates are actionable and based on best practices in cost estimation.
78
+
79
+ Please provide a detailed cost estimate for each task, including minimum, maximum, and realistic costs,
80
+ along with a breakdown of cost components and any relevant assumptions or risks.
81
+
82
+ Cost components with smaller quantities
83
+ Round up the partial-hour rates to the nearest whole hour.
84
+ If a meeting is 15 minutes, the bill might be 1-hour. Better to overestimate than underestimate.
85
+
86
+ Here are the details of the project tasks for cost estimation:
87
+
88
+ """
89
+
90
+ @dataclass
91
+ class ExpertCost:
92
+ """
93
+ Ask an expert advise about estimating cost.
94
+ """
95
+ query: str
96
+ response: dict
97
+ metadata: dict
98
+
99
+ @classmethod
100
+ def format_system(cls, expert: dict) -> str:
101
+ if not isinstance(expert, dict):
102
+ raise ValueError("Invalid expert.")
103
+
104
+ role = expert.get('title', 'Cost Estimation Expert')
105
+ knowledge = expert.get('knowledge', 'Cost estimation methodologies, project budgeting, financial analysis.')
106
+ skills = expert.get('skills', 'Analytical skills, attention to detail, proficiency in budgeting tools.')
107
+
108
+ query = f"""
109
+ You are acting as a highly experienced {role}.
110
+
111
+ Your areas of deep knowledge include:
112
+ {knowledge}
113
+
114
+ You possess the following key skills:
115
+ {skills}
116
+
117
+ """
118
+ return query
119
+
120
+ @classmethod
121
+ def format_query(cls, currency: str, location: str, task_ids_to_process: list[str], documents: list[Document]) -> str:
122
+ if not isinstance(currency, str):
123
+ raise ValueError("Invalid currency.")
124
+ if not isinstance(location, str):
125
+ raise ValueError("Invalid location.")
126
+ if not isinstance(task_ids_to_process, list):
127
+ raise ValueError("Invalid task_ids_to_process.")
128
+ if not isinstance(documents, list):
129
+ raise ValueError("Invalid documents.")
130
+
131
+ task_ids_in_quotes = [f'"{task_id}"' for task_id in task_ids_to_process]
132
+ task_id_strings = "\n".join(task_ids_in_quotes)
133
+ task_id_count = len(task_ids_to_process)
134
+
135
+ document_items = []
136
+ for document_index, document in enumerate(documents, start=1):
137
+ document_items.append(f"File {document_index}, {document.name}:\n{document.content}")
138
+
139
+ document_content = "\n\n".join(document_items)
140
+ query = f"""
141
+ {document_content}
142
+
143
+ Extra information:
144
+ - All cost estimates should be in {currency}.
145
+ - The project is located in {location}; consider local market rates and economic factors.
146
+
147
+ Please provide exactly one cost estimate for each of the following {task_id_count} tasks and no others:
148
+ {task_id_strings}
149
+ **Do not** include cost estimates for tasks not in this list.
150
+ """
151
+ return query
152
+
153
+ @classmethod
154
+ def execute(cls, llm: LLM, query: str, system_prompt: Optional[str]) -> 'ExpertCost':
155
+ """
156
+ Invoke LLM to get cost estimation advice from the expert.
157
+ """
158
+ if not isinstance(llm, LLM):
159
+ raise ValueError("Invalid LLM instance.")
160
+ if not isinstance(query, str):
161
+ raise ValueError("Invalid query.")
162
+
163
+ chat_message_list = []
164
+ if system_prompt:
165
+ chat_message_list.append(
166
+ ChatMessage(
167
+ role=MessageRole.SYSTEM,
168
+ content=system_prompt,
169
+ )
170
+ )
171
+
172
+ chat_message_user = ChatMessage(
173
+ role=MessageRole.USER,
174
+ content=query,
175
+ )
176
+ chat_message_list.append(chat_message_user)
177
+
178
+ start_time = time.perf_counter()
179
+
180
+ sllm = llm.as_structured_llm(ExpertCostEstimationResponse)
181
+ chat_response = sllm.chat(chat_message_list)
182
+ json_response = json.loads(chat_response.message.content)
183
+
184
+ end_time = time.perf_counter()
185
+ duration = int(ceil(end_time - start_time))
186
+
187
+ metadata = dict(llm.metadata)
188
+ metadata["llm_classname"] = llm.class_name()
189
+ metadata["duration"] = duration
190
+
191
+ result = ExpertCost(
192
+ query=query,
193
+ response=json_response,
194
+ metadata=metadata,
195
+ )
196
+ return result
197
+
198
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
199
+ d = self.response.copy()
200
+ if include_metadata:
201
+ d['metadata'] = self.metadata
202
+ if include_query:
203
+ d['query'] = self.query
204
+ return d
205
+
206
+ if __name__ == "__main__":
207
+ from llama_index.llms.ollama import Ollama
208
+ from llama_index.llms.openai_like import OpenAILike
209
+ from dotenv import dotenv_values
210
+ import os
211
+ from wbs_table_for_cost_estimation.wbs_table_for_cost_estimation import WBSTableForCostEstimation
212
+ from chunk_dataframe_with_context.chunk_dataframe_with_context import chunk_dataframe_with_context
213
+ import pandas as pd
214
+ from pandas import DataFrame
215
+
216
+ dotenv_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '.env'))
217
+ dotenv_dict = dotenv_values(dotenv_path=dotenv_path)
218
+
219
+ if True:
220
+ model_name = "llama3.1:latest"
221
+ # model_name = "qwen2.5-coder:latest"
222
+ # model_name = "phi4:latest"
223
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
224
+ else:
225
+ llm = OpenAILike(
226
+ api_base="https://api.deepseek.com/v1",
227
+ api_key=dotenv_dict['DEEPSEEK_API_KEY'],
228
+ model="deepseek-chat",
229
+ is_chat_model=True,
230
+ is_function_calling_model=True,
231
+ max_retries=1,
232
+ )
233
+
234
+
235
+ # TODO: Eliminate hardcoded paths
236
+ basepath = '/Users/neoneye/Desktop/planexe_data'
237
+
238
+ def load_json(relative_path: str) -> dict:
239
+ path = os.path.join(basepath, relative_path)
240
+ print(f"loading file: {path}")
241
+ with open(path, 'r', encoding='utf-8') as f:
242
+ the_json = json.load(f)
243
+ return the_json
244
+
245
+ def load_text(relative_path: str) -> dict:
246
+ path = os.path.join(basepath, relative_path)
247
+ print(f"loading file: {path}")
248
+ with open(path, 'r', encoding='utf-8') as f:
249
+ the_text = f.read()
250
+ return the_text
251
+
252
+ plan_txt = load_text('001-plan.txt')
253
+ document_plan = Document(name="vague_plan_description.txt", content=plan_txt)
254
+
255
+ project_plan_json = load_json('002-project_plan.json')
256
+ project_plan = format_json_for_use_in_query(project_plan_json)
257
+ document_project_plan = Document(name="project_plan.json", content=project_plan)
258
+
259
+ swot_analysis_md = load_text('004-swot_analysis.md')
260
+ document_swot_analysis = Document(name="swot_analysis.md", content=swot_analysis_md)
261
+
262
+ expert_list_json = load_json('006-experts.json')
263
+
264
+ path_wbs_table_csv = os.path.join(basepath, '016-wbs_table.csv')
265
+ path_wbs_project_json = os.path.join(basepath, '016-wbs_project.json')
266
+ wbs_table = WBSTableForCostEstimation.create(path_wbs_table_csv, path_wbs_project_json)
267
+ wbs_df = wbs_table.wbs_table_df.copy()
268
+
269
+ expert = expert_list_json[5]
270
+ expert.pop('id')
271
+ system_prompt = ExpertCost.format_system(expert)
272
+ print(f"System: {system_prompt}")
273
+
274
+ currency = "DKK"
275
+ location = "Kolonihave at Kongelundsvej, Copenhagen, Denmark"
276
+
277
+ # The LLM cannot handle the entire WBS hierarchy at once, usually more than 100 rows.
278
+ # Instead process the CSV in chunks of N rows.
279
+ chunk_size=3
280
+ overlap=4
281
+
282
+ # Collect all chunks in a list to know how many there are
283
+ all_chunks = list(chunk_dataframe_with_context(wbs_df, chunk_size, overlap))
284
+ # truncate to 5 chunks
285
+ all_chunks = all_chunks[:5]
286
+
287
+ # Print out the total number of chunks (iterations) that will be processed
288
+ number_of_chunks = len(all_chunks)
289
+ print(f"There will be {number_of_chunks} iterations.")
290
+
291
+ documents_static = [document_plan, document_project_plan, document_swot_analysis]
292
+
293
+ # Then iterate over them as usual
294
+ for chunk_index, (core_df, extended_df) in enumerate(all_chunks, start=1):
295
+ print(f"Processing chunk {chunk_index} of {number_of_chunks} ...")
296
+
297
+ # Convert extended_df to CSV for the LLM prompt
298
+ extended_csv = extended_df.to_csv(sep=';', index=False)
299
+ document_wbs_chunk = Document(name="work_breakdown_structure.csv", content=extended_csv)
300
+
301
+ # The tasks we want cost-estimated in this chunk (core tasks only)
302
+ task_ids_to_process = core_df['Task ID'].tolist()
303
+
304
+ # Format the query with extended context as the content,
305
+ # but instruct the LLM to only produce estimates for the
306
+ # `task_ids_to_process`.
307
+ query = ExpertCost.format_query(
308
+ currency=currency,
309
+ location=location,
310
+ task_ids_to_process=task_ids_to_process,
311
+ documents=documents_static + [document_wbs_chunk],
312
+ )
313
+
314
+ # Make the LLM call
315
+ print(f"\n\nChunk {chunk_index} Query (len={len(query)}): {query}")
316
+ # print(f"\n\nChunk {chunk_index} Execute. len(query)={len(query)}")
317
+ result = ExpertCost.execute(llm, query, system_prompt)
318
+
319
+ print(f"\n\nChunk {chunk_index} Response:")
320
+ print(json.dumps(result.raw_response_dict(include_query=False), indent=2))
src/plan/filenames.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+ class FilenameEnum(str, Enum):
4
+ INITIAL_PLAN = "001-plan.txt"
5
+ MAKE_ASSUMPTIONS_RAW = "002-make_assumptions_raw.json"
6
+ MAKE_ASSUMPTIONS = "003-make_assumptions.json"
7
+ DISTILL_ASSUMPTIONS_RAW = "004-distill_assumptions.json"
8
+ PRE_PROJECT_ASSESSMENT_RAW = "005-pre_project_assessment_raw.json"
9
+ PRE_PROJECT_ASSESSMENT = "006-pre_project_assessment.json"
10
+ PROJECT_PLAN = "007-project_plan.json"
11
+ SWOT_RAW = "008-swot_analysis_raw.json"
12
+ SWOT_MARKDOWN = "009-swot_analysis.md"
13
+ EXPERTS_RAW = "010-experts_raw.json"
14
+ EXPERTS_CLEAN = "011-experts.json"
15
+ EXPERT_CRITICISM_RAW_TEMPLATE = "012-{}-expert_criticism_raw.json"
16
+ EXPERT_CRITICISM_MARKDOWN = "013-expert_criticism.md"
17
+ WBS_LEVEL1_RAW = "014-wbs_level1_raw.json"
18
+ WBS_LEVEL1 = "015-wbs_level1.json"
19
+ WBS_LEVEL2_RAW = "016-wbs_level2_raw.json"
20
+ WBS_LEVEL2 = "017-wbs_level2.json"
21
+ WBS_PROJECT_LEVEL1_AND_LEVEL2 = "018-wbs_project_level1_and_level2.json"
22
+ PITCH = "019-pitch.json"
23
+ TASK_DEPENDENCIES_RAW = "020-task_dependencies_raw.json"
24
+ TASK_DURATIONS_RAW_TEMPLATE = "021-{}-task_durations_raw.json"
25
+ TASK_DURATIONS = "022-task_durations.json"
26
+ WBS_LEVEL3_RAW_TEMPLATE = "023-{}-wbs_level3_raw.json"
27
+ WBS_LEVEL3 = "024-wbs_level3.json"
28
+ WBS_PROJECT_LEVEL1_AND_LEVEL2_AND_LEVEL3_FULL = "025-wbs_project_level1_and_level2_and_level3.json"
29
+ WBS_PROJECT_LEVEL1_AND_LEVEL2_AND_LEVEL3_CSV = "026-wbs_project_level1_and_level2_and_level3.csv"
30
+ PIPELINE_COMPLETE = "999-pipeline_complete.txt"
src/plan/find_plan_prompt.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.prompt.prompt_catalog import PromptCatalog
2
+ import os
3
+
4
+ def find_plan_prompt(prompt_id: str) -> str:
5
+ prompt_catalog = PromptCatalog()
6
+ prompt_catalog.load(os.path.join(os.path.dirname(__file__), 'data', 'simple_plan_prompts.jsonl'))
7
+ prompt_item = prompt_catalog.find(prompt_id)
8
+ if not prompt_item:
9
+ raise ValueError(f"Prompt ID '{prompt_id}' not found.")
10
+ return prompt_item.prompt
src/plan/identify_wbs_task_dependencies.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Identified dependencies, that serves as a foundation for task sequencing.
3
+ https://en.wikipedia.org/wiki/Work_breakdown_structure
4
+
5
+ IDEA: I'm not happy about have two separate arrays for the task_ids and the explanations.
6
+ There should be 1 task id and 1 explanation per dependency.
7
+ Currently there can be N task ids and M explanations, where N != M. This is not good.
8
+
9
+ IDEA: Label each dependency with its type (FS, SS, FF, SF).
10
+ - Finish-to-Start (FS): Task B cannot start until Task A is finished (most common).
11
+ - Start-to-Start (SS): Task B cannot start until Task A starts.
12
+ - Finish-to-Finish (FF): Task B cannot finish until Task A is finished.
13
+ - Start-to-Finish (SF): Task B cannot finish until Task A starts (least common).
14
+
15
+ IDEA: Missing Dependencies.
16
+ I asked Gemini to check the json file containing the dependencies, and it did spot some missing dependencies.
17
+ So I need an extra LLM that can go identify if there are more missing dependencies.
18
+ """
19
+ import os
20
+ import json
21
+ import time
22
+ from math import ceil
23
+ from typing import List, Optional
24
+ from uuid import uuid4
25
+ from dataclasses import dataclass
26
+ from pydantic import BaseModel, Field
27
+ from llama_index.core.llms.llm import LLM
28
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
29
+
30
+ class TaskDependencyDetail(BaseModel):
31
+ """
32
+ Details about the prerequisites for a task.
33
+ """
34
+ dependent_task_id: str = Field(
35
+ description="UUID that uniquely identifies a major phase or a subtask."
36
+ )
37
+
38
+ depends_on_task_id_list: list[str] = Field(
39
+ description="List of UUIDs that are prerequisites for this task."
40
+ )
41
+
42
+ depends_on_task_explanation_list: list[str] = Field(
43
+ description="List of explanations why these tasks must be completed before this task."
44
+ )
45
+
46
+ class DependencyMapping(BaseModel):
47
+ """
48
+ Understanding the dependencies between tasks is crucial for effective project scheduling and
49
+ ensuring that prerequisites are met before commencing subsequent activities.
50
+ """
51
+ task_dependency_details: list[TaskDependencyDetail] = Field(
52
+ description="List with dependency mappings between tasks."
53
+ )
54
+
55
+ QUERY_PREAMBLE = f"""
56
+ Find the 10 most critical important task dependencies. Don't attempt making an exhaustive list.
57
+
58
+ Understanding how tasks relate to each other is crucial for accurate timeline planning.
59
+ Dependencies determine the sequence in which tasks must be completed.
60
+
61
+ Types of Dependencies:
62
+ • Finish-to-Start (FS): Task B cannot start until Task A is finished.
63
+ • Start-to-Start (SS): Task B cannot start until Task A starts.
64
+ • Finish-to-Finish (FF): Task B cannot finish until Task A finishes.
65
+ • Start-to-Finish (SF): Task B cannot finish until Task A starts (rarely used).
66
+
67
+ Example Dependencies:
68
+ • Land Acquisition must be completed before Permitting and Approvals can begin.
69
+ • Permitting and Approvals must be completed before Design and Engineering starts.
70
+ • Procurement of materials can begin once Design and Engineering is underway.
71
+
72
+ """
73
+
74
+ @dataclass
75
+ class IdentifyWBSTaskDependencies:
76
+ """
77
+ Enrich an existing Work Breakdown Structure (WBS) with details about dependencies between tasks.
78
+ """
79
+ query: str
80
+ response: dict
81
+ metadata: dict
82
+
83
+ @classmethod
84
+ def format_query(cls, plan_json: dict, wbs_level2_json: list) -> str:
85
+ """
86
+ Format the query for creating a Work Breakdown Structure (WBS) level 2.
87
+ """
88
+ if not isinstance(plan_json, dict):
89
+ raise ValueError("Invalid plan_json.")
90
+ if not isinstance(wbs_level2_json, list):
91
+ raise ValueError("Invalid wbs_list.")
92
+
93
+ query = f"""
94
+ The project plan:
95
+ {format_json_for_use_in_query(plan_json)}
96
+
97
+ The Work Breakdown Structure (WBS):
98
+ {format_json_for_use_in_query(wbs_level2_json)}
99
+ """
100
+ return query
101
+
102
+ @classmethod
103
+ def execute(cls, llm: LLM, query: str) -> 'IdentifyWBSTaskDependencies':
104
+ """
105
+ Invoke LLM to identify task dependencies from a json representation of a project plan and Work Breakdown Structure (WBS).
106
+ """
107
+ if not isinstance(llm, LLM):
108
+ raise ValueError("Invalid LLM instance.")
109
+ if not isinstance(query, str):
110
+ raise ValueError("Invalid query.")
111
+
112
+ start_time = time.perf_counter()
113
+
114
+ sllm = llm.as_structured_llm(DependencyMapping)
115
+ response = sllm.complete(QUERY_PREAMBLE + query)
116
+ json_response = json.loads(response.text)
117
+
118
+ end_time = time.perf_counter()
119
+ duration = int(ceil(end_time - start_time))
120
+
121
+ metadata = dict(llm.metadata)
122
+ metadata["llm_classname"] = llm.class_name()
123
+ metadata["duration"] = duration
124
+
125
+ result = IdentifyWBSTaskDependencies(
126
+ query=query,
127
+ response=json_response,
128
+ metadata=metadata
129
+ )
130
+ return result
131
+
132
+ def raw_response_dict(self, include_metadata=True, include_query=True) -> dict:
133
+ d = self.response.copy()
134
+ if include_metadata:
135
+ d['metadata'] = self.metadata
136
+ if include_query:
137
+ d['query'] = self.query
138
+ return d
139
+
140
+ if __name__ == "__main__":
141
+ from llama_index.llms.ollama import Ollama
142
+ # TODO: Eliminate hardcoded paths
143
+ basepath = '/Users/neoneye/Desktop/planexe_data'
144
+
145
+ def load_json(relative_path: str) -> dict:
146
+ path = os.path.join(basepath, relative_path)
147
+ print(f"loading file: {path}")
148
+ with open(path, 'r', encoding='utf-8') as f:
149
+ the_json = json.load(f)
150
+ return the_json
151
+
152
+ plan_json = load_json('002-project_plan.json')
153
+ wbs_json = load_json('005-wbs_level2.json')
154
+
155
+ query = IdentifyWBSTaskDependencies.format_query(plan_json, wbs_json)
156
+
157
+ model_name = "llama3.1:latest"
158
+ # model_name = "qwen2.5-coder:latest"
159
+ # model_name = "phi4:latest"
160
+ llm = Ollama(model=model_name, request_timeout=120.0, temperature=0.5, is_function_calling_model=False)
161
+
162
+ print(f"Query: {query}")
163
+ result = IdentifyWBSTaskDependencies.execute(llm, query)
164
+
165
+ print("Response:")
166
+ response_dict = result.raw_response_dict(include_query=False)
167
+ print(json.dumps(response_dict, indent=2))
src/plan/plan_file.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.plan.plan_file
3
+ """
4
+ from datetime import datetime
5
+ from dataclasses import dataclass
6
+
7
+ @dataclass
8
+ class PlanFile:
9
+ content: str
10
+
11
+ @classmethod
12
+ def create(cls, vague_plan_description: str) -> "PlanFile":
13
+ run_date = datetime.now()
14
+ pretty_date = run_date.strftime("%Y-%b-%d")
15
+ plan_prompt = (
16
+ f"Plan:\n{vague_plan_description}\n\n"
17
+ f"Today's date:\n{pretty_date}\n\n"
18
+ "Project start ASAP"
19
+ )
20
+ return cls(plan_prompt)
21
+
22
+ def save(self, file_path: str) -> None:
23
+ with open(file_path, "w") as f:
24
+ f.write(self.content)
25
+
26
+ if __name__ == "__main__":
27
+ plan = PlanFile.create("My plan is here!")
28
+ print(plan.content)
src/plan/run_plan_pipeline.py ADDED
@@ -0,0 +1,934 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PROMPT> python -m src.plan.run_plan_pipeline
3
+ """
4
+ from datetime import datetime
5
+ import logging
6
+ import json
7
+ import luigi
8
+ from pathlib import Path
9
+
10
+ from src.plan.filenames import FilenameEnum
11
+ from src.plan.speedvsdetail import SpeedVsDetailEnum
12
+ from src.plan.plan_file import PlanFile
13
+ from src.plan.find_plan_prompt import find_plan_prompt
14
+ from src.assume.make_assumptions import MakeAssumptions
15
+ from src.assume.assumption_orchestrator import AssumptionOrchestrator
16
+ from src.expert.pre_project_assessment import PreProjectAssessment
17
+ from src.plan.create_project_plan import CreateProjectPlan
18
+ from src.swot.swot_analysis import SWOTAnalysis
19
+ from src.expert.expert_finder import ExpertFinder
20
+ from src.expert.expert_criticism import ExpertCriticism
21
+ from src.expert.expert_orchestrator import ExpertOrchestrator
22
+ from src.plan.create_wbs_level1 import CreateWBSLevel1
23
+ from src.plan.create_wbs_level2 import CreateWBSLevel2
24
+ from src.plan.create_wbs_level3 import CreateWBSLevel3
25
+ from src.plan.create_pitch import CreatePitch
26
+ from src.plan.identify_wbs_task_dependencies import IdentifyWBSTaskDependencies
27
+ from src.plan.estimate_wbs_task_durations import EstimateWBSTaskDurations
28
+ from src.wbs.wbs_task import WBSTask, WBSProject
29
+ from src.wbs.wbs_populate import WBSPopulate
30
+ from src.llm_factory import get_llm
31
+ from src.format_json_for_use_in_query import format_json_for_use_in_query
32
+ from src.utils.get_env_as_string import get_env_as_string
33
+
34
+ logger = logging.getLogger(__name__)
35
+ DEFAULT_LLM_MODEL = "ollama-llama3.1"
36
+
37
+ class PlanTask(luigi.Task):
38
+ # Default it to the current timestamp, eg. 19841231_235959
39
+ run_id = luigi.Parameter(default=datetime.now().strftime("%Y%m%d_%H%M%S"))
40
+
41
+ # By default, run everything but it's slow.
42
+ # This can be overridden in developer mode, where a quick turnaround is needed, and the details are not important.
43
+ speedvsdetail = luigi.EnumParameter(enum=SpeedVsDetailEnum, default=SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW)
44
+
45
+ @property
46
+ def run_dir(self) -> Path:
47
+ return Path('run') / self.run_id
48
+
49
+ def file_path(self, filename: FilenameEnum) -> Path:
50
+ return self.run_dir / filename.value
51
+
52
+
53
+ class SetupTask(PlanTask):
54
+ def output(self):
55
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.INITIAL_PLAN)))
56
+
57
+ def run(self):
58
+ # Ensure the run directory exists.
59
+ self.run_dir.mkdir(parents=True, exist_ok=True)
60
+
61
+ # Pick a random prompt.
62
+ plan_prompt = find_plan_prompt("4dc34d55-0d0d-4e9d-92f4-23765f49dd29")
63
+ plan_file = PlanFile.create(plan_prompt)
64
+ plan_file.save(self.output().path)
65
+
66
+ class AssumptionsTask(PlanTask):
67
+ """
68
+ Make assumptions about the plan.
69
+ Depends on:
70
+ - SetupTask (for the initial plan)
71
+ """
72
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
73
+
74
+ def requires(self):
75
+ return SetupTask(run_id=self.run_id)
76
+
77
+ def output(self):
78
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.DISTILL_ASSUMPTIONS_RAW)))
79
+
80
+ def run(self):
81
+ logger.info("Making assumptions about the plan...")
82
+
83
+ # Read inputs from required tasks.
84
+ with self.input().open("r") as f:
85
+ plan_prompt = f.read()
86
+
87
+ # I'm currently debugging the speedvsdetail parameter. When I'm done I can remove it.
88
+ # Verifying that the speedvsdetail parameter is set correctly.
89
+ logger.info(f"AssumptionsTask.speedvsdetail: {self.speedvsdetail}")
90
+ if self.speedvsdetail == SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS:
91
+ logger.info("AssumptionsTask: We are in FAST_BUT_SKIP_DETAILS mode.")
92
+ else:
93
+ logger.info("AssumptionsTask: We are in another mode")
94
+
95
+ llm = get_llm(self.llm_model)
96
+
97
+ # Define callback functions.
98
+ def phase1_post_callback(make_assumptions: MakeAssumptions) -> None:
99
+ raw_path = self.run_dir / FilenameEnum.MAKE_ASSUMPTIONS_RAW.value
100
+ cleaned_path = self.run_dir / FilenameEnum.MAKE_ASSUMPTIONS.value
101
+ make_assumptions.save_raw(str(raw_path))
102
+ make_assumptions.save_assumptions(str(cleaned_path))
103
+
104
+ # Execute
105
+ orchestrator = AssumptionOrchestrator()
106
+ orchestrator.phase1_post_callback = phase1_post_callback
107
+ orchestrator.execute(llm, plan_prompt)
108
+
109
+ # Write the assumptions to the output file.
110
+ file_path = self.run_dir / FilenameEnum.DISTILL_ASSUMPTIONS_RAW.value
111
+ orchestrator.distill_assumptions.save_raw(str(file_path))
112
+
113
+
114
+ class PreProjectAssessmentTask(PlanTask):
115
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
116
+
117
+ def requires(self):
118
+ return SetupTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail)
119
+
120
+ def output(self):
121
+ return {
122
+ 'raw': luigi.LocalTarget(str(self.file_path(FilenameEnum.PRE_PROJECT_ASSESSMENT_RAW))),
123
+ 'clean': luigi.LocalTarget(str(self.file_path(FilenameEnum.PRE_PROJECT_ASSESSMENT)))
124
+ }
125
+
126
+ def run(self):
127
+ logger.info("Conducting pre-project assessment...")
128
+
129
+ # Read the plan prompt from the SetupTask's output.
130
+ with self.input().open("r") as f:
131
+ plan_prompt = f.read()
132
+
133
+ # Build the query.
134
+ query = f"Initial plan: {plan_prompt}\n\n"
135
+
136
+ # Get an instance of your LLM.
137
+ llm = get_llm(self.llm_model)
138
+
139
+ # Execute the pre-project assessment.
140
+ pre_project_assessment = PreProjectAssessment.execute(llm, query)
141
+
142
+ # Save raw output.
143
+ raw_path = self.file_path(FilenameEnum.PRE_PROJECT_ASSESSMENT_RAW)
144
+ pre_project_assessment.save_raw(str(raw_path))
145
+
146
+ # Save cleaned pre-project assessment.
147
+ clean_path = self.file_path(FilenameEnum.PRE_PROJECT_ASSESSMENT)
148
+ pre_project_assessment.save_preproject_assessment(str(clean_path))
149
+
150
+
151
+ class ProjectPlanTask(PlanTask):
152
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
153
+
154
+ def requires(self):
155
+ """
156
+ This task depends on:
157
+ - SetupTask: produces the plan prompt (001-plan.txt)
158
+ - AssumptionsTask: produces the distilled assumptions (003-distill_assumptions.json)
159
+ - PreProjectAssessmentTask: produces the pre‑project assessment files
160
+ """
161
+ return {
162
+ 'setup': SetupTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail),
163
+ 'assumptions': AssumptionsTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
164
+ 'preproject': PreProjectAssessmentTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
165
+ }
166
+
167
+ def output(self):
168
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.PROJECT_PLAN)))
169
+
170
+ def run(self):
171
+ logger.info("Creating plan...")
172
+
173
+ # Read the plan prompt from SetupTask's output.
174
+ setup_target = self.input()['setup']
175
+ with setup_target.open("r") as f:
176
+ plan_prompt = f.read()
177
+
178
+ # Read assumptions from the distilled assumptions output.
179
+ assumptions_file = self.input()['assumptions']
180
+ with assumptions_file.open("r") as f:
181
+ assumption_list = json.load(f)
182
+
183
+ # Read the pre-project assessment from its file.
184
+ pre_project_assessment_file = self.input()['preproject']['clean']
185
+ with pre_project_assessment_file.open("r") as f:
186
+ pre_project_assessment_dict = json.load(f)
187
+
188
+ # Build the query.
189
+ query = (
190
+ f"Initial plan: {plan_prompt}\n\n"
191
+ f"Assumptions:\n{format_json_for_use_in_query(assumption_list)}\n\n"
192
+ f"Pre-project assessment:\n{format_json_for_use_in_query(pre_project_assessment_dict)}"
193
+ )
194
+
195
+ # Get an LLM instance.
196
+ llm = get_llm(self.llm_model)
197
+
198
+ # Execute the plan creation.
199
+ create_project_plan = CreateProjectPlan.execute(llm, query)
200
+ output_path = self.output().path
201
+ create_project_plan.save(output_path)
202
+
203
+ logger.info("Project plan created and saved to %s", output_path)
204
+
205
+
206
+ class SWOTAnalysisTask(PlanTask):
207
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
208
+
209
+ def requires(self):
210
+ return {
211
+ 'setup': SetupTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail),
212
+ 'assumptions': AssumptionsTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
213
+ 'preproject': PreProjectAssessmentTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
214
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
215
+ }
216
+
217
+ def output(self):
218
+ return {
219
+ 'raw': luigi.LocalTarget(str(self.file_path(FilenameEnum.SWOT_RAW))),
220
+ 'markdown': luigi.LocalTarget(str(self.file_path(FilenameEnum.SWOT_MARKDOWN)))
221
+ }
222
+
223
+ def run(self):
224
+ logger.info("SWOTAnalysisTask. Loading files...")
225
+
226
+ # 1. Read the plan prompt from SetupTask.
227
+ with self.input()['setup'].open("r") as f:
228
+ plan_prompt = f.read()
229
+
230
+ # 2. Read the distilled assumptions from AssumptionsTask.
231
+ with self.input()['assumptions'].open("r") as f:
232
+ assumption_list = json.load(f)
233
+
234
+ # 3. Read the pre-project assessment from PreProjectAssessmentTask.
235
+ with self.input()['preproject']['clean'].open("r") as f:
236
+ pre_project_assessment_dict = json.load(f)
237
+
238
+ # 4. Read the project plan from ProjectPlanTask.
239
+ with self.input()['project_plan'].open("r") as f:
240
+ project_plan_dict = json.load(f)
241
+
242
+ logger.info("SWOTAnalysisTask. All files are now ready. Performing analysis...")
243
+
244
+ # Build the query for SWOT analysis.
245
+ query = (
246
+ f"Initial plan: {plan_prompt}\n\n"
247
+ f"Assumptions:\n{format_json_for_use_in_query(assumption_list)}\n\n"
248
+ f"Pre-project assessment:\n{format_json_for_use_in_query(pre_project_assessment_dict)}\n\n"
249
+ f"Project plan:\n{format_json_for_use_in_query(project_plan_dict)}"
250
+ )
251
+
252
+ # Create LLM instances for SWOT analysis.
253
+ llm = get_llm(self.llm_model)
254
+
255
+ # Execute the SWOT analysis.
256
+ try:
257
+ swot_analysis = SWOTAnalysis.execute(llm, query)
258
+ except Exception as e:
259
+ logger.error("SWOT analysis failed: %s", e)
260
+ raise
261
+
262
+ # Convert the SWOT analysis to a dict and markdown.
263
+ swot_raw_dict = swot_analysis.to_dict()
264
+ swot_markdown = swot_analysis.to_markdown(include_metadata=False)
265
+
266
+ # Write the raw SWOT JSON.
267
+ with self.output()['raw'].open("w") as f:
268
+ json.dump(swot_raw_dict, f, indent=2)
269
+
270
+ # Write the SWOT analysis as Markdown.
271
+ with self.output()['markdown'].open("w") as f:
272
+ f.write(swot_markdown)
273
+
274
+ logger.info("SWOT analysis complete.")
275
+
276
+ class ExpertReviewTask(PlanTask):
277
+ """
278
+ Finds experts to review the SWOT analysis and have them provide criticism.
279
+ Depends on:
280
+ - SetupTask (for the initial plan)
281
+ - PreProjectAssessmentTask (for the pre‑project assessment)
282
+ - ProjectPlanTask (for the project plan)
283
+ - SWOTAnalysisTask (for the SWOT analysis)
284
+ Produces:
285
+ - Raw experts file (006-experts_raw.json)
286
+ - Cleaned experts file (007-experts.json)
287
+ - For each expert, a raw expert criticism file (008-XX-expert_criticism_raw.json) [side effects via callbacks]
288
+ - Final expert criticism markdown (009-expert_criticism.md)
289
+ """
290
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
291
+
292
+ def requires(self):
293
+ return {
294
+ 'setup': SetupTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail),
295
+ 'preproject': PreProjectAssessmentTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
296
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
297
+ 'swot_analysis': SWOTAnalysisTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
298
+ }
299
+
300
+ def output(self):
301
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.EXPERT_CRITICISM_MARKDOWN)))
302
+
303
+ def run(self):
304
+ logger.info("Finding experts to review the SWOT analysis, and having them provide criticism...")
305
+
306
+ # Read inputs from required tasks.
307
+ with self.input()['setup'].open("r") as f:
308
+ plan_prompt = f.read()
309
+ with self.input()['preproject']['clean'].open("r") as f:
310
+ pre_project_assessment_dict = json.load(f)
311
+ with self.input()['project_plan'].open("r") as f:
312
+ project_plan_dict = json.load(f)
313
+ with self.input()['swot_analysis']['markdown'].open("r") as f:
314
+ swot_markdown = f.read()
315
+
316
+ # Build the query.
317
+ query = (
318
+ f"Initial plan: {plan_prompt}\n\n"
319
+ f"Pre-project assessment:\n{format_json_for_use_in_query(pre_project_assessment_dict)}\n\n"
320
+ f"Project plan:\n{format_json_for_use_in_query(project_plan_dict)}\n\n"
321
+ f"SWOT Analysis:\n{swot_markdown}"
322
+ )
323
+
324
+ llm = get_llm(self.llm_model)
325
+
326
+ # Define callback functions.
327
+ def phase1_post_callback(expert_finder: ExpertFinder) -> None:
328
+ raw_path = self.run_dir / FilenameEnum.EXPERTS_RAW.value
329
+ cleaned_path = self.run_dir / FilenameEnum.EXPERTS_CLEAN.value
330
+ expert_finder.save_raw(str(raw_path))
331
+ expert_finder.save_cleanedup(str(cleaned_path))
332
+
333
+ def phase2_post_callback(expert_criticism: ExpertCriticism, expert_index: int) -> None:
334
+ file_path = self.run_dir / FilenameEnum.EXPERT_CRITICISM_RAW_TEMPLATE.format(expert_index + 1)
335
+ expert_criticism.save_raw(str(file_path))
336
+
337
+ # Execute the expert orchestration.
338
+ expert_orchestrator = ExpertOrchestrator()
339
+ # IDEA: max_expert_count. don't truncate to 2 experts. Interview them all in production mode.
340
+ expert_orchestrator.phase1_post_callback = phase1_post_callback
341
+ expert_orchestrator.phase2_post_callback = phase2_post_callback
342
+ expert_orchestrator.execute(llm, query)
343
+
344
+ # Write final expert criticism markdown.
345
+ expert_criticism_markdown_file = self.file_path(FilenameEnum.EXPERT_CRITICISM_MARKDOWN)
346
+ with expert_criticism_markdown_file.open("w") as f:
347
+ f.write(expert_orchestrator.to_markdown())
348
+
349
+ class CreateWBSLevel1Task(PlanTask):
350
+ """
351
+ Creates the Work Breakdown Structure (WBS) Level 1.
352
+ Depends on:
353
+ - ProjectPlanTask: provides the project plan as JSON.
354
+ Produces:
355
+ - Raw WBS Level 1 output file (xxx-wbs_level1_raw.json)
356
+ - Cleaned up WBS Level 1 file (xxx-wbs_level1.json)
357
+ """
358
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
359
+
360
+ def requires(self):
361
+ return {
362
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
363
+ }
364
+
365
+ def output(self):
366
+ return {
367
+ 'raw': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_LEVEL1_RAW))),
368
+ 'clean': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_LEVEL1)))
369
+ }
370
+
371
+ def run(self):
372
+ logger.info("Creating Work Breakdown Structure (WBS) Level 1...")
373
+
374
+ # Read the project plan JSON from the dependency.
375
+ with self.input()['project_plan'].open("r") as f:
376
+ project_plan_dict = json.load(f)
377
+
378
+ # Build the query using the project plan.
379
+ query = format_json_for_use_in_query(project_plan_dict)
380
+
381
+ # Get an LLM instance.
382
+ llm = get_llm(self.llm_model)
383
+
384
+ # Execute the WBS Level 1 creation.
385
+ create_wbs_level1 = CreateWBSLevel1.execute(llm, query)
386
+
387
+ # Save the raw output.
388
+ wbs_level1_raw_dict = create_wbs_level1.raw_response_dict()
389
+ with self.output()['raw'].open("w") as f:
390
+ json.dump(wbs_level1_raw_dict, f, indent=2)
391
+
392
+ # Save the cleaned up result.
393
+ wbs_level1_result_json = create_wbs_level1.cleanedup_dict()
394
+ with self.output()['clean'].open("w") as f:
395
+ json.dump(wbs_level1_result_json, f, indent=2)
396
+
397
+ logger.info("WBS Level 1 created successfully.")
398
+
399
+ class CreateWBSLevel2Task(PlanTask):
400
+ """
401
+ Creates the Work Breakdown Structure (WBS) Level 2.
402
+ Depends on:
403
+ - ProjectPlanTask: provides the project plan as JSON.
404
+ - CreateWBSLevel1Task: provides the cleaned WBS Level 1 result.
405
+ Produces:
406
+ - Raw WBS Level 2 output (007-wbs_level2_raw.json)
407
+ - Cleaned WBS Level 2 output (008-wbs_level2.json)
408
+ """
409
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
410
+
411
+ def requires(self):
412
+ return {
413
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
414
+ 'wbs_level1': CreateWBSLevel1Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
415
+ }
416
+
417
+ def output(self):
418
+ return {
419
+ 'raw': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_LEVEL2_RAW))),
420
+ 'clean': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_LEVEL2)))
421
+ }
422
+
423
+ def run(self):
424
+ logger.info("Creating Work Breakdown Structure (WBS) Level 2...")
425
+
426
+ # Read the project plan from the ProjectPlanTask output.
427
+ with self.input()['project_plan'].open("r") as f:
428
+ project_plan_dict = json.load(f)
429
+
430
+ # Read the cleaned WBS Level 1 result from the CreateWBSLevel1Task output.
431
+ # Here we assume the cleaned output is under the 'clean' key.
432
+ with self.input()['wbs_level1']['clean'].open("r") as f:
433
+ wbs_level1_result_json = json.load(f)
434
+
435
+ # Build the query using CreateWBSLevel2's format_query method.
436
+ query = CreateWBSLevel2.format_query(project_plan_dict, wbs_level1_result_json)
437
+
438
+ # Get an LLM instance.
439
+ llm = get_llm(self.llm_model)
440
+
441
+ # Execute the WBS Level 2 creation.
442
+ create_wbs_level2 = CreateWBSLevel2.execute(llm, query)
443
+
444
+ # Retrieve and write the raw output.
445
+ wbs_level2_raw_dict = create_wbs_level2.raw_response_dict()
446
+ with self.output()['raw'].open("w") as f:
447
+ json.dump(wbs_level2_raw_dict, f, indent=2)
448
+
449
+ # Retrieve and write the cleaned output (e.g. major phases with subtasks).
450
+ with self.output()['clean'].open("w") as f:
451
+ json.dump(create_wbs_level2.major_phases_with_subtasks, f, indent=2)
452
+
453
+ logger.info("WBS Level 2 created successfully.")
454
+
455
+ class WBSProjectLevel1AndLevel2Task(PlanTask):
456
+ """
457
+ Create a WBS project from the WBS Level 1 and Level 2 JSON files.
458
+
459
+ It depends on:
460
+ - CreateWBSLevel1Task: providing the cleaned WBS Level 1 JSON.
461
+ - CreateWBSLevel2Task: providing the major phases with subtasks and the task UUIDs.
462
+ """
463
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
464
+
465
+ def output(self):
466
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_PROJECT_LEVEL1_AND_LEVEL2)))
467
+
468
+ def requires(self):
469
+ return {
470
+ 'wbs_level1': CreateWBSLevel1Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
471
+ 'wbs_level2': CreateWBSLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
472
+ }
473
+
474
+ def run(self):
475
+ wbs_level1_path = self.input()['wbs_level1']['clean'].path
476
+ wbs_level2_path = self.input()['wbs_level2']['clean'].path
477
+ wbs_project = WBSPopulate.project_from_level1_json(wbs_level1_path)
478
+ WBSPopulate.extend_project_with_level2_json(wbs_project, wbs_level2_path)
479
+
480
+ json_representation = json.dumps(wbs_project.to_dict(), indent=2)
481
+ with self.output().open("w") as f:
482
+ f.write(json_representation)
483
+
484
+ class CreatePitchTask(PlanTask):
485
+ """
486
+ Create a the pitch that explains the project plan, from multiple perspectives.
487
+
488
+ This task depends on:
489
+ - ProjectPlanTask: provides the project plan JSON.
490
+ - WBSProjectLevel1AndLevel2Task: containing the top level of the project plan.
491
+
492
+ The resulting pitch JSON is written to the file specified by FilenameEnum.PITCH.
493
+ """
494
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
495
+
496
+ def output(self):
497
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.PITCH)))
498
+
499
+ def requires(self):
500
+ return {
501
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
502
+ 'wbs_project': WBSProjectLevel1AndLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
503
+ }
504
+
505
+ def run(self):
506
+ logger.info("Creating pitch...")
507
+
508
+ # Read the project plan JSON.
509
+ with self.input()['project_plan'].open("r") as f:
510
+ project_plan_dict = json.load(f)
511
+
512
+ wbs_project_path = self.input()['wbs_project'].path
513
+ with open(wbs_project_path, "r") as f:
514
+ wbs_project_dict = json.load(f)
515
+ wbs_project = WBSProject.from_dict(wbs_project_dict)
516
+ wbs_project_json = wbs_project.to_dict()
517
+
518
+ # Build the query
519
+ query = (
520
+ f"The project plan:\n{format_json_for_use_in_query(project_plan_dict)}\n\n"
521
+ f"Work Breakdown Structure:\n{format_json_for_use_in_query(wbs_project_json)}"
522
+ )
523
+
524
+ # Get the LLM instance.
525
+ llm = get_llm(self.llm_model)
526
+
527
+ # Execute the pitch creation.
528
+ create_pitch = CreatePitch.execute(llm, query)
529
+ pitch_dict = create_pitch.raw_response_dict()
530
+
531
+ # Write the resulting pitch JSON to the output file.
532
+ with self.output().open("w") as f:
533
+ json.dump(pitch_dict, f, indent=2)
534
+
535
+ logger.info("Pitch created and written to %s", self.output().path)
536
+
537
+ class IdentifyTaskDependenciesTask(PlanTask):
538
+ """
539
+ This task identifies the dependencies between WBS tasks.
540
+
541
+ It depends on:
542
+ - ProjectPlanTask: provides the project plan JSON.
543
+ - CreateWBSLevel2Task: provides the major phases with subtasks.
544
+
545
+ The raw JSON response is written to the file specified by FilenameEnum.TASK_DEPENDENCIES_RAW.
546
+ """
547
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
548
+
549
+ def output(self):
550
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.TASK_DEPENDENCIES_RAW)))
551
+
552
+ def requires(self):
553
+ return {
554
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
555
+ 'wbs_level2': CreateWBSLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
556
+ }
557
+
558
+ def run(self):
559
+ logger.info("Identifying task dependencies...")
560
+
561
+ # Read the project plan JSON.
562
+ with self.input()['project_plan'].open("r") as f:
563
+ project_plan_dict = json.load(f)
564
+
565
+ # Read the major phases with subtasks from WBS Level 2 output.
566
+ with self.input()['wbs_level2']['clean'].open("r") as f:
567
+ major_phases_with_subtasks = json.load(f)
568
+
569
+ # Build the query using the provided format method.
570
+ query = IdentifyWBSTaskDependencies.format_query(project_plan_dict, major_phases_with_subtasks)
571
+
572
+ # Get the LLM instance.
573
+ llm = get_llm(self.llm_model)
574
+
575
+ # Execute the dependency identification.
576
+ identify_dependencies = IdentifyWBSTaskDependencies.execute(llm, query)
577
+ dependencies_raw_dict = identify_dependencies.raw_response_dict()
578
+
579
+ # Write the raw dependencies JSON to the output file.
580
+ with self.output().open("w") as f:
581
+ json.dump(dependencies_raw_dict, f, indent=2)
582
+
583
+ logger.info("Task dependencies identified and written to %s", self.output().path)
584
+
585
+ class EstimateTaskDurationsTask(PlanTask):
586
+ """
587
+ This task estimates durations for WBS tasks in chunks.
588
+
589
+ It depends on:
590
+ - ProjectPlanTask: providing the project plan JSON.
591
+ - WBSProjectLevel1AndLevel2Task: providing the major phases with subtasks and the task UUIDs.
592
+
593
+ For each chunk of 3 task IDs, a raw JSON file (e.g. "011-1-task_durations_raw.json") is written,
594
+ and an aggregated JSON file (defined by FilenameEnum.TASK_DURATIONS) is produced.
595
+
596
+ IDEA: 1st estimate the Tasks that have zero children.
597
+ 2nd estimate tasks that have children where all children have been estimated.
598
+ repeat until all tasks have been estimated.
599
+ """
600
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
601
+
602
+ def output(self):
603
+ # The primary output is the aggregated task durations JSON.
604
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.TASK_DURATIONS)))
605
+
606
+ def requires(self):
607
+ return {
608
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
609
+ 'wbs_project': WBSProjectLevel1AndLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
610
+ }
611
+
612
+ def run(self):
613
+ logger.info("Estimating task durations...")
614
+
615
+ # Load the project plan JSON.
616
+ with self.input()['project_plan'].open("r") as f:
617
+ project_plan_dict = json.load(f)
618
+
619
+ with self.input()['wbs_project'].open("r") as f:
620
+ wbs_project_dict = json.load(f)
621
+ wbs_project = WBSProject.from_dict(wbs_project_dict)
622
+
623
+ # json'ish representation of the major phases in the WBS, and their subtasks.
624
+ root_task = wbs_project.root_task
625
+ major_tasks = [child.to_dict() for child in root_task.task_children]
626
+ major_phases_with_subtasks = major_tasks
627
+
628
+ # Don't include uuid of the root task. It's the child tasks that are of interest to estimate.
629
+ decompose_task_id_list = []
630
+ for task in wbs_project.root_task.task_children:
631
+ decompose_task_id_list.extend(task.task_ids())
632
+
633
+ logger.info(f"There are {len(decompose_task_id_list)} tasks to be estimated.")
634
+
635
+ # Split the task IDs into chunks of 3.
636
+ task_ids_chunks = [decompose_task_id_list[i:i + 3] for i in range(0, len(decompose_task_id_list), 3)]
637
+
638
+ # In production mode, all chunks are processed.
639
+ # In developer mode, truncate to only 2 chunks for fast turnaround cycle. Otherwise LOTS of tasks are to be estimated.
640
+ logger.info(f"EstimateTaskDurationsTask.speedvsdetail: {self.speedvsdetail}")
641
+ if self.speedvsdetail == SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS:
642
+ logger.info("FAST_BUT_SKIP_DETAILS mode, truncating to 2 chunks for testing.")
643
+ task_ids_chunks = task_ids_chunks[:2]
644
+ else:
645
+ logger.info("Processing all chunks.")
646
+
647
+ # Get the LLM instance.
648
+ llm = get_llm(self.llm_model)
649
+
650
+ # Process each chunk.
651
+ accumulated_task_duration_list = []
652
+ for index, task_ids_chunk in enumerate(task_ids_chunks, start=1):
653
+ logger.info("Processing chunk %d of %d", index, len(task_ids_chunks))
654
+
655
+ query = EstimateWBSTaskDurations.format_query(
656
+ project_plan_dict,
657
+ major_phases_with_subtasks,
658
+ task_ids_chunk
659
+ )
660
+
661
+ estimate_durations = EstimateWBSTaskDurations.execute(llm, query)
662
+ durations_raw_dict = estimate_durations.raw_response_dict()
663
+
664
+ # Write the raw JSON for this chunk.
665
+ filename = FilenameEnum.TASK_DURATIONS_RAW_TEMPLATE.format(index)
666
+ raw_chunk_path = self.run_dir / filename
667
+ with open(raw_chunk_path, "w") as f:
668
+ json.dump(durations_raw_dict, f, indent=2)
669
+
670
+ accumulated_task_duration_list.extend(durations_raw_dict.get('task_details', []))
671
+
672
+ # Write the aggregated task durations.
673
+ aggregated_path = self.file_path(FilenameEnum.TASK_DURATIONS)
674
+ with open(aggregated_path, "w") as f:
675
+ json.dump(accumulated_task_duration_list, f, indent=2)
676
+
677
+ logger.info("Task durations estimated and aggregated results written to %s", aggregated_path)
678
+
679
+ class CreateWBSLevel3Task(PlanTask):
680
+ """
681
+ This task creates the Work Breakdown Structure (WBS) Level 3, by decomposing tasks from Level 2 into subtasks.
682
+
683
+ It depends on:
684
+ - ProjectPlanTask: provides the project plan JSON.
685
+ - WBSProjectLevel1AndLevel2Task: provides the major phases with subtasks and the task UUIDs.
686
+ - EstimateTaskDurationsTask: provides the aggregated task durations (task_duration_list).
687
+
688
+ For each task without any subtasks, a query is built and executed using the LLM.
689
+ The raw JSON result for each task is written to a file using the template from FilenameEnum.
690
+ Finally, all individual results are accumulated and written as an aggregated JSON file.
691
+ """
692
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
693
+
694
+ def output(self):
695
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_LEVEL3)))
696
+
697
+ def requires(self):
698
+ return {
699
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
700
+ 'wbs_project': WBSProjectLevel1AndLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
701
+ 'task_durations': EstimateTaskDurationsTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model)
702
+ }
703
+
704
+ def run(self):
705
+ logger.info("Creating Work Breakdown Structure (WBS) Level 3...")
706
+
707
+ # Load the project plan JSON.
708
+ with self.input()['project_plan'].open("r") as f:
709
+ project_plan_dict = json.load(f)
710
+
711
+ with self.input()['wbs_project'].open("r") as f:
712
+ wbs_project_dict = json.load(f)
713
+ wbs_project = WBSProject.from_dict(wbs_project_dict)
714
+
715
+ # Load the estimated task durations.
716
+ task_duration_list_path = self.input()['task_durations'].path
717
+ WBSPopulate.extend_project_with_durations_json(wbs_project, task_duration_list_path)
718
+
719
+ # for each task in the wbs_project, find the task that has no children
720
+ tasks_with_no_children = []
721
+ def visit_task(task):
722
+ if len(task.task_children) == 0:
723
+ tasks_with_no_children.append(task)
724
+ else:
725
+ for child in task.task_children:
726
+ visit_task(child)
727
+ visit_task(wbs_project.root_task)
728
+
729
+ # for each task with no children, extract the task_id
730
+ decompose_task_id_list = []
731
+ for task in tasks_with_no_children:
732
+ decompose_task_id_list.append(task.id)
733
+
734
+ logger.info("There are %d tasks to be decomposed.", len(decompose_task_id_list))
735
+
736
+ # In production mode, all chunks are processed.
737
+ # In developer mode, truncate to only 2 chunks for fast turnaround cycle. Otherwise LOTS of tasks are to be decomposed.
738
+ logger.info(f"CreateWBSLevel3Task.speedvsdetail: {self.speedvsdetail}")
739
+ if self.speedvsdetail == SpeedVsDetailEnum.FAST_BUT_SKIP_DETAILS:
740
+ logger.info("FAST_BUT_SKIP_DETAILS mode, truncating to 2 chunks for testing.")
741
+ decompose_task_id_list = decompose_task_id_list[:2]
742
+ else:
743
+ logger.info("Processing all chunks.")
744
+
745
+ # Get an LLM instance.
746
+ llm = get_llm(self.llm_model)
747
+
748
+ project_plan_str = format_json_for_use_in_query(project_plan_dict)
749
+ wbs_project_str = format_json_for_use_in_query(wbs_project.to_dict())
750
+
751
+ # Loop over each task ID.
752
+ wbs_level3_result_accumulated = []
753
+ total_tasks = len(decompose_task_id_list)
754
+ for index, task_id in enumerate(decompose_task_id_list, start=1):
755
+ logger.info("Decomposing task %d of %d", index, total_tasks)
756
+
757
+ query = (
758
+ f"The project plan:\n{project_plan_str}\n\n"
759
+ f"Work breakdown structure:\n{wbs_project_str}\n\n"
760
+ f"Only decompose this task:\n\"{task_id}\""
761
+ )
762
+
763
+ create_wbs_level3 = CreateWBSLevel3.execute(llm, query, task_id)
764
+ wbs_level3_raw_dict = create_wbs_level3.raw_response_dict()
765
+
766
+ # Write the raw JSON for this task using the FilenameEnum template.
767
+ raw_filename = FilenameEnum.WBS_LEVEL3_RAW_TEMPLATE.value.format(index)
768
+ raw_chunk_path = self.run_dir / raw_filename
769
+ with open(raw_chunk_path, 'w') as f:
770
+ json.dump(wbs_level3_raw_dict, f, indent=2)
771
+
772
+ # Accumulate the decomposed tasks.
773
+ wbs_level3_result_accumulated.extend(create_wbs_level3.tasks)
774
+
775
+ # Write the aggregated WBS Level 3 result.
776
+ aggregated_path = self.file_path(FilenameEnum.WBS_LEVEL3)
777
+ with open(aggregated_path, 'w') as f:
778
+ json.dump(wbs_level3_result_accumulated, f, indent=2)
779
+
780
+ logger.info("WBS Level 3 created and aggregated results written to %s", aggregated_path)
781
+
782
+ class WBSProjectLevel1AndLevel2AndLevel3Task(PlanTask):
783
+ """
784
+ Create a WBS project from the WBS Level 1 and Level 2 and Level 3 JSON files.
785
+
786
+ It depends on:
787
+ - WBSProjectLevel1AndLevel2Task: providing the major phases with subtasks and the task UUIDs.
788
+ - CreateWBSLevel3Task: providing the decomposed tasks.
789
+ """
790
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
791
+
792
+ def output(self):
793
+ return {
794
+ 'full': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_PROJECT_LEVEL1_AND_LEVEL2_AND_LEVEL3_FULL))),
795
+ 'csv': luigi.LocalTarget(str(self.file_path(FilenameEnum.WBS_PROJECT_LEVEL1_AND_LEVEL2_AND_LEVEL3_CSV)))
796
+ }
797
+
798
+ def requires(self):
799
+ return {
800
+ 'wbs_project12': WBSProjectLevel1AndLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
801
+ 'wbs_level3': CreateWBSLevel3Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
802
+ }
803
+
804
+ def run(self):
805
+ wbs_project_path = self.input()['wbs_project12'].path
806
+ with open(wbs_project_path, "r") as f:
807
+ wbs_project_dict = json.load(f)
808
+ wbs_project = WBSProject.from_dict(wbs_project_dict)
809
+
810
+ wbs_level3_path = self.input()['wbs_level3'].path
811
+ WBSPopulate.extend_project_with_decomposed_tasks_json(wbs_project, wbs_level3_path)
812
+
813
+ json_representation = json.dumps(wbs_project.to_dict(), indent=2)
814
+ with self.output()['full'].open("w") as f:
815
+ f.write(json_representation)
816
+
817
+ csv_representation = wbs_project.to_csv_string()
818
+ with self.output()['csv'].open("w") as f:
819
+ f.write(csv_representation)
820
+
821
+ class FullPlanPipeline(PlanTask):
822
+ llm_model = luigi.Parameter(default=DEFAULT_LLM_MODEL)
823
+
824
+ def requires(self):
825
+ return {
826
+ 'setup': SetupTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail),
827
+ 'assumptions': AssumptionsTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
828
+ 'pre_project_assessment': PreProjectAssessmentTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
829
+ 'project_plan': ProjectPlanTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
830
+ 'swot_analysis': SWOTAnalysisTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
831
+ 'expert_review': ExpertReviewTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
832
+ 'wbs_level1': CreateWBSLevel1Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
833
+ 'wbs_level2': CreateWBSLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
834
+ 'wbs_project12': WBSProjectLevel1AndLevel2Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
835
+ 'pitch': CreatePitchTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
836
+ 'dependencies': IdentifyTaskDependenciesTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
837
+ 'durations': EstimateTaskDurationsTask(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
838
+ 'wbs_level3': CreateWBSLevel3Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
839
+ 'wbs_project123': WBSProjectLevel1AndLevel2AndLevel3Task(run_id=self.run_id, speedvsdetail=self.speedvsdetail, llm_model=self.llm_model),
840
+ }
841
+
842
+ def output(self):
843
+ return luigi.LocalTarget(str(self.file_path(FilenameEnum.PIPELINE_COMPLETE)))
844
+
845
+ def run(self):
846
+ with self.output().open("w") as f:
847
+ f.write("Full pipeline executed successfully.\n")
848
+
849
+
850
+ if __name__ == '__main__':
851
+ import colorlog
852
+ import sys
853
+ import os
854
+
855
+ run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
856
+
857
+ # specify a hardcoded, and it will resume work on that directory
858
+ # run_id = "20250205_141025"
859
+
860
+ # if env contains "RUN_ID" then use that as the run_id
861
+ if "RUN_ID" in os.environ:
862
+ run_id = os.environ["RUN_ID"]
863
+
864
+ run_dir = os.path.join("run", run_id)
865
+ os.makedirs(run_dir, exist_ok=True)
866
+
867
+ logger = logging.getLogger()
868
+ logger.setLevel(logging.DEBUG)
869
+
870
+ # Log messages on the console
871
+ colored_formatter = colorlog.ColoredFormatter(
872
+ "%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s",
873
+ datefmt='%Y-%m-%d %H:%M:%S',
874
+ log_colors={
875
+ 'DEBUG': 'cyan',
876
+ 'INFO': 'green',
877
+ 'WARNING': 'yellow',
878
+ 'ERROR': 'red',
879
+ 'CRITICAL': 'red,bg_white',
880
+ }
881
+ )
882
+ stdout_handler = colorlog.StreamHandler(stream=sys.stdout)
883
+ stdout_handler.setFormatter(colored_formatter)
884
+ stdout_handler.setLevel(logging.DEBUG)
885
+ logger.addHandler(stdout_handler)
886
+
887
+ # Capture logs messages to 'run/yyyymmdd_hhmmss/log.txt'
888
+ file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
889
+ log_file = os.path.join(run_dir, "log.txt")
890
+ file_handler = logging.FileHandler(log_file, mode='w')
891
+ file_handler.setLevel(logging.DEBUG)
892
+ file_handler.setFormatter(file_formatter)
893
+ logger.addHandler(file_handler)
894
+
895
+ logger.info(f"run_id: {run_id}")
896
+
897
+ # Example logging messages
898
+ if False:
899
+ logger.debug("This is a debug message.")
900
+ logger.info("This is an info message.")
901
+ logger.warning("This is a warning message.")
902
+ logger.error("This is an error message.")
903
+ logger.critical("This is a critical message.")
904
+
905
+ model = DEFAULT_LLM_MODEL # works
906
+ model = "openrouter-paid-gemini-2.0-flash-001" # works
907
+ # model = "openrouter-paid-openai-gpt-4o-mini" # often fails, I think it's not good at structured output
908
+
909
+ if "LLM_MODEL" in os.environ:
910
+ model = os.environ["LLM_MODEL"]
911
+
912
+ logger.info(f"LLM model: {model}")
913
+
914
+ speedvsdetail = SpeedVsDetailEnum.ALL_DETAILS_BUT_SLOW
915
+ if "SPEED_VS_DETAIL" in os.environ:
916
+ speedvsdetail_value = os.environ["SPEED_VS_DETAIL"]
917
+ found = False
918
+ for e in SpeedVsDetailEnum:
919
+ if e.value == speedvsdetail_value:
920
+ speedvsdetail = e
921
+ found = True
922
+ logger.info(f"Setting Speed vs Detail: {speedvsdetail}")
923
+ break
924
+ if not found:
925
+ logger.error(f"Invalid value for SPEED_VS_DETAIL: {speedvsdetail_value}")
926
+ logger.info(f"Speed vs Detail: {speedvsdetail}")
927
+
928
+ task = FullPlanPipeline(speedvsdetail=speedvsdetail, llm_model=model)
929
+ if run_id is not None:
930
+ task.run_id = run_id
931
+
932
+ # logger.info("Environment variables Luigi:\n" + get_env_as_string() + "\n\n\n")
933
+
934
+ luigi.build([task], local_scheduler=True, workers=1)
src/plan/speedvsdetail.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+ class SpeedVsDetailEnum(str, Enum):
4
+ # Production mode.
5
+ ALL_DETAILS_BUT_SLOW = "all_details_but_slow"
6
+
7
+ # Developer mode that does a quick check if the pipeline runs. The accuracy is not a priority.
8
+ FAST_BUT_SKIP_DETAILS = "fast_but_skip_details"
src/prompt/__init__.py ADDED
File without changes
src/prompt/prompt_catalog.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Dict, Any, Optional
5
+ from src.uuid_util.is_valid_uuid import is_valid_uuid
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ @dataclass
10
+ class PromptItem:
11
+ """Dataclass to hold a single prompt with tags, UUID, and any extra fields."""
12
+ id: str
13
+ prompt: str
14
+ tags: List[str] = field(default_factory=list)
15
+ extras: Dict[str, Any] = field(default_factory=dict)
16
+
17
+ class PromptCatalog:
18
+ """
19
+ A catalog of PromptItem objects, keyed by UUID.
20
+ Supports loading from one or more JSONL files, each containing
21
+ one JSON object per line.
22
+ """
23
+ def __init__(self):
24
+ self._catalog: Dict[str, PromptItem] = {}
25
+
26
+ def load(self, filepath: str) -> None:
27
+ """
28
+ Load prompts from a JSONL file. Each line is expected to have
29
+ fields like 'id', 'prompt', 'tags', etc.
30
+ Logs an error if 'id' or 'prompt' is missing/empty, then skips that row.
31
+ """
32
+ with open(filepath, 'r', encoding='utf-8') as f:
33
+ for line_num, line in enumerate(f, start=1):
34
+ line = line.strip()
35
+ if not line:
36
+ continue
37
+
38
+ try:
39
+ data = json.loads(line)
40
+ except json.JSONDecodeError as e:
41
+ logger.error(f"JSON decode error in {filepath} at line {line_num}: {e}")
42
+ continue
43
+
44
+ pid = data.get('id')
45
+ prompt_text = data.get('prompt')
46
+
47
+ if not pid:
48
+ logger.error(f"Missing 'id' field in {filepath} at line {line_num}. Skipping row.")
49
+ continue
50
+ if not prompt_text:
51
+ logger.error(f"Missing or empty 'prompt' for ID '{pid}' in {filepath} at line {line_num}. Skipping row.")
52
+ continue
53
+
54
+ if not is_valid_uuid(pid):
55
+ logger.error(f"Invalid UUID in {filepath} at line {line_num}: {pid}. Skipping row.")
56
+ continue
57
+
58
+ tags = data.get('tags', [])
59
+ extras = {k: v for k, v in data.items() if k not in ('id', 'prompt', 'tags')}
60
+
61
+ if self._catalog.get(pid):
62
+ logger.error(f"Duplicate UUID found in {filepath} at line {line_num}: {pid}. Skipping row.")
63
+ continue
64
+
65
+ item = PromptItem(id=pid, prompt=prompt_text, tags=tags, extras=extras)
66
+ self._catalog[pid] = item
67
+
68
+ def find(self, prompt_id: str) -> Optional[PromptItem]:
69
+ """Retrieve a PromptItem by its ID (UUID). Returns None if not found."""
70
+ if not is_valid_uuid(prompt_id):
71
+ raise ValueError(f"Invalid UUID: {prompt_id}")
72
+ return self._catalog.get(prompt_id)
73
+
74
+ def find_by_tag(self, tag: str) -> List[PromptItem]:
75
+ """
76
+ Return a list of all PromptItems that contain the given tag
77
+ (case-sensitive match).
78
+ """
79
+ return [item for item in self._catalog.values() if tag in item.tags]
80
+
81
+ def all(self) -> List[PromptItem]:
82
+ """Return a list of all PromptItems in the order they were inserted."""
83
+ return list(self._catalog.values())
src/prompt/test_data/prompts_simple.jsonl ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ {"id":"cfd7aaf3-b521-42c6-ae50-6f0ecbc0c6ca","prompt":"I'm a prompt with 3 tags","tags":["tag1","tag2","tag3"]}
2
+ {"id":"25bd2b32-ac7c-4b71-ba55-a7c6e29d08c5","prompt":"I'm a prompt with an extra field named 'comment'","tags":["I'm a tag"],"comment":"I'm a comment"}
src/prompt/tests/__init__.py ADDED
File without changes