Name: Towards AI Legal Name: Towards AI, Inc. Description: Towards AI is the world's leading artificial intelligence (AI) and technology publication. Read by thought-leaders and decision-makers around the world. Phone Number: +1-650-246-9381 Email: pub@towardsai.net
228 Park Avenue South New York, NY 10003 United States
Website: Publisher: https://towardsai.net/#publisher Diversity Policy: https://towardsai.net/about Ethics Policy: https://towardsai.net/about Masthead: https://towardsai.net/about
Name: Towards AI Legal Name: Towards AI, Inc. Description: Towards AI is the world's leading artificial intelligence (AI) and technology publication. Founders: Roberto Iriondo, , Job Title: Co-founder and Advisor Works for: Towards AI, Inc. Follow Roberto: X, LinkedIn, GitHub, Google Scholar, Towards AI Profile, Medium, ML@CMU, FreeCodeCamp, Crunchbase, Bloomberg, Roberto Iriondo, Generative AI Lab, Generative AI Lab Denis Piffaretti, Job Title: Co-founder Works for: Towards AI, Inc. Louie Peters, Job Title: Co-founder Works for: Towards AI, Inc. Louis-François Bouchard, Job Title: Co-founder Works for: Towards AI, Inc. Cover:
Towards AI Cover
Logo:
Towards AI Logo
Areas Served: Worldwide Alternate Name: Towards AI, Inc. Alternate Name: Towards AI Co. Alternate Name: towards ai Alternate Name: towardsai Alternate Name: towards.ai Alternate Name: tai Alternate Name: toward ai Alternate Name: toward.ai Alternate Name: Towards AI, Inc. Alternate Name: towardsai.net Alternate Name: pub.towardsai.net
5 stars – based on 497 reviews

Frequently Used, Contextual References

TODO: Remember to copy unique IDs whenever it needs used. i.e., URL: 304b2e42315e

Resources

Take our 85+ lesson From Beginner to Advanced LLM Developer Certification: From choosing a project to deploying a working product this is the most comprehensive and practical LLM course out there!

Publication

Managing an AI developer: Lessons Learned from SMOL AI — Part 2
Latest   Machine Learning

Managing an AI developer: Lessons Learned from SMOL AI — Part 2

Last Updated on June 28, 2023 by Editorial Team

Author(s): Meir Kanevskiy

Originally published on Towards AI.

Source: Image by DALL-E

In Part 1 of our exploration into managing an AI agent developer, we embarked on an experiment to tackle the challenges that arise in overseeing an automated workforce. We entrusted the role of the “junior developer” to SMOL AI’s rapidly evolving developer, as detailed in its readme:

a prototype of a “junior developer” agent (aka smol dev).

The focal point of our experiment was a specific problem statement: the need to generate a naming scheme that follows the conventions of running Docker containers. We provided two prompts to the AI agent: the initial prompt outlined the requirement for a function that generates random names consisting of an adjective and a noun,

Please write a naming scheme generator function that generates a random name
to the likes of running docker containers, consisting of an adjective and
a noun, where adjective is an emotional description, e.g. dramatic,
and noun is an abstract entity like manifold.
I thas to contain up to 7 adjective options for every letter and
up to 7 nouns for every letter.

While the second prompt introduced the customization of theme pairs by allowing users to provide their own JSON file with options for each letter of the alphabet and featured much more elaborate and formalized project specifications:

This project is a naming scheme generator.
It has to be subject to the following tech stack:
1. Programming language: wherever possible, but not limited to python
It has to be subject to the following specification:
1. Having an importable and callable method in python that returns a random name
2. Said name has to be a pair of an adjective and a noun starting with the same letter to the likes of docker container naming convention
3. Adjective and noun have to be of specific theme. Default themes should emotional description, e.g. dramatic for adjective and an abstract entity like manifold for noun.
4. Apart from the default themes, theme pair should be customizable by providing a json with list of available options per each letter of the English alphabet. Providing the json can be constrained to a certain folder inside the project structure. Said json should be explicitly read by python's json library to a dictionary and then used throughout other modules of the project
5. it has to have tests in pytest, evaluating extensiveness of fulfilling the above specifications

Through this experiment, we delved into the complexities of managing an AI agent, highlighting the significance of comprehending the agent’s thinking process and providing appropriate guidance. The second prompt let us achieve significant improvement in core functionality and also added testing omitted by default.

This point in our development experiment would be a fitting one to explore the question of optimal stopping. An analogy from a different AI application would be what is known in autonomous driving as a takeover or intervention event, where the human driver takes over the steering wheel from the autopilot. However, unlike in autonomous driving, where intervention is considered a catastrophic failure due to the clear safety implications, such interventions might be commonplace in automated development for years to come, if not indefinitely. While autonomous driving agent has a closed-form purpose of getting passengers from A to B, maximizing safety and speed, in all types of creative labor, there might be a threshold of idea origination unachievable by an automated agent. Even if this is not the case — supervision and responsibility over the final delivery would likely be on the human supervisor, if not in the role of a more senior developer, then as an equivalent of the current project manager/team lead.

With this in mind, let’s explore 2 different paths from where we have stopped in Part 1:

  1. We will try to finalize the project ourselves
  2. We will ask smol dev to iterate on the result, slightly modifying the prompt

Takeover

Functional code

A first glance at the test runs leaves much to be desired. As expected, due to import issues, none of the tests ran. Firstly, we will correct the obvious linting mistakes and import mistakes. JSON import is omitted, and necessary JSON reads are misplaced as Python imports:

--- a/smol_ai_developer/generated_namer2/naming_scheme_generator/generator.py
+++ b/smol_ai_developer/generated_namer2/naming_scheme_generator/generator.py
@@ -1,25 +1,23 @@
+import json

--- a/smol_ai_developer/generated_namer2/naming_scheme_generator/themes/__init__.py
+++ b/smol_ai_developer/generated_namer2/naming_scheme_generator/themes/__init__.py
@@ -1,2 +1,7 @@
-from .default_theme import default_theme
-from .custom_theme_example import custom_theme_example
+import json
+
+with open("./naming_scheme_generator/themes/default_theme.json") as f:
+ default_theme = json.load(f)
+
+with open("./naming_scheme_generator/themes/custom_theme_example.json") as f:
+ custom_theme_example = json.load(f)

Upon a second glance, we can see how deeply the misplaced dictionary levels go. We need to unify them to fit the ‘letter > nouns/adjectives’ structure, not the other way around:

--- a/smol_ai_developer/generated_namer2/naming_scheme_generator/generator.py
+++ b/smol_ai_developer/generated_namer2/naming_scheme_generator/generator.py
@@ -1,25 +1,23 @@
+import json
import random
from typing import Dict, List
from .themes import default_theme

-def generate_name(theme_adjective: Dict[str, List[str]], theme_noun: Dict[str, List[str]]) -> str:
- letter = random.choice(list(theme_adjective.keys()))
- adjective = random.choice(theme_adjective[letter])
- noun = random.choice(theme_noun[letter])
+def generate_name(theme) -> str:
+ letter = random.choice(list(theme.keys()))
+ adjective = random.choice(theme[letter]['adjectives'])
+ noun = random.choice(theme[letter]['nouns'])
return f"{adjective}_{noun}"

-def load_custom_theme(file_path: str) -> Dict[str, List[str]]:
+def load_custom_theme(file_path: str):
with open(file_path, "r") as file:
custom_theme = json.load(file)
return custom_theme

def generate_name_with_theme(theme: str = "default") -> str:
if theme == "default":
- theme_adjective = default_theme["adjectives"]
- theme_noun = default_theme["nouns"]
+ theme = default_theme
else:
custom_theme = load_custom_theme(theme)
- theme_adjective = custom_theme["adjectives"]
- theme_noun = custom_theme["nouns"]

- return generate_name(theme_adjective, theme_noun)
+ return generate_name(theme)

In smol dev’s defense we must say that control flow is arguably correct and, strictly speaking, optional, function typings are (almost) as well.

At this point, our module is actually working:

In [1]: from naming_scheme_generator.generator import generate_name_with_theme

In [2]: large_name_list = [generate_name_with_theme() for _ in range(100)]

In [3]: len(set(large_name_list))
Out[3]: 62

In [4]: large_name_list[42]
Out[4]: 'zen_zephyr'

This, actually, is substantial, as we corrected two glaring mistakes we have identified immediately at the initial review of the results in Part 1. As we’ll shortly see, there was room for much more subtle mistakes.

As it was included in the specification, let’s take a look at the tests.

Testing

We have created two sets of tests, effectively distinguishing between main generation functionality and theme customization. Both files have several issues, which can largely be divided into two groups:

  1. Technical issues, such as using generated_name.split(" "), while the generator file explicitly uses a hard-coded "_" between an adjective and a noun. All of these cases can be resolved with a simple replace function. More nuanced examples would include contradicting function signatures from the main module, which in Python are optional (in fact, in some instances, signatures are incorrect, while their output usage in tests is accurate).
  2. Design issues, like referencing default_theme.adjectives or self.assertIsInstance(custom_theme, themes.Theme).

The second issue is arguably a symptom of a more significant problem we’ve noted from the beginning: not only can the dev agent fail to maintain consistent indexing in the nested dictionary of our theme structure, but sometimes it can incorrectly assume a different structure altogether. While writing the tests, the agent made an erroneous assumption that a theme is not a dictionary at all, but a custom object with attributed nouns and adjectives.

In other words, what our agent likely lacks is an explicit design specification.

These issues necessitate substantial, but ultimately trivial, corrections in the testing code, as the tested functions’ signatures do not require any specifications of the theme object’s interface at all:

@@ -5,19 +5,19 @@ from naming_scheme_generator.themes import default_theme
class TestGenerator(unittest.TestCase):

def test_generate_name_default_theme(self):
- name = generate_name(default_theme.adjectives, default_theme.nouns)
+ name = generate_name(default_theme)
self.assertIsNotNone(name)
self.assertTrue(isinstance(name, str))
- self.assertEqual(len(name.split(" ")), 2)
- self.assertEqual(name.split(" ")[0][0], name.split(" ")[1][0])
+ self.assertEqual(len(name.split("_")), 2)
+ self.assertEqual(name.split("_")[0][0], name.split("_")[1][0])

def test_generate_name_custom_theme(self):
custom_theme = load_custom_theme("naming_scheme_generator/themes/custom_theme_example.json")
- name = generate_name(custom_theme.adjectives, custom_theme.nouns)
+ name = generate_name(custom_theme)
self.assertIsNotNone(name)
self.assertTrue(isinstance(name, str))
- self.assertEqual(len(name.split(" ")), 2)
- self.assertEqual(name.split(" ")[0][0], name.split(" ")[1][0])
+ self.assertEqual(len(name.split("_")), 2)
+ self.assertEqual(name.split("_")[0][0], name.split("_")[1][0])
@@ -7,33 +7,34 @@ class TestThemes(unittest.TestCase):

def test_load_custom_theme(self):
custom_theme_path = "naming_scheme_generator/themes/custom_theme_example.json"
- custom_theme = themes.load_custom_theme(custom_theme_path)
- self.assertIsInstance(custom_theme, themes.Theme)
+ custom_theme = generator.load_custom_theme(custom_theme_path)
+ self.assertIsInstance(custom_theme, dict)

with open(custom_theme_path, "r") as f:
custom_theme_data = json.load(f)

for letter, theme_data in custom_theme_data.items():
- self.assertEqual(custom_theme.adjectives[letter], theme_data["adjectives"])
- self.assertEqual(custom_theme.nouns[letter], theme_data["nouns"])
+ self.assertEqual(custom_theme[letter]['adjectives'], theme_data["adjectives"])
+ self.assertEqual(custom_theme[letter]['nouns'], theme_data["nouns"])

def test_generate_name_default_theme(self):
default_theme = themes.default_theme
- generated_name = generator.generate_name(default_theme.adjectives, default_theme.nouns)
+ generated_name = generator.generate_name(default_theme)
self.assertIsInstance(generated_name, str)
- self.assertEqual(len(generated_name.split(" ")), 2)
+ self.assertEqual(len(generated_name.split("_")), 2)

- adjective, noun = generated_name.split(" ")
+ adjective, noun = generated_name.split("_")
self.assertEqual(adjective[0], noun[0])

def test_generate_name_custom_theme(self):
custom_theme_path = "naming_scheme_generator/themes/custom_theme_example.json"
- custom_theme = themes.load_custom_theme(custom_theme_path)
- generated_name = generator.generate_name(custom_theme.adjectives, custom_theme.nouns)
+ custom_theme = generator.load_custom_theme(custom_theme_path)
+ generated_name = generator.generate_name(custom_theme)
+ print(generated_name)
self.assertIsInstance(generated_name, str)
- self.assertEqual(len(generated_name.split(" ")), 2)
+ self.assertEqual(len(generated_name.split("_")), 2)

- adjective, noun = generated_name.split(" ")
+ adjective, noun = generated_name.split("_")

Agent’s self-correction

This time, we will not amend the prompt. While we want to allow the dev agent to improve on its previous result, we deliberately avoid expanding on specific design details mentioned above, as doing so would shift our role from general product ownership to a more involved development role. This iteration, however, was a surprising failure — the overridden repo now looks like this:

├── naming_scheme_generator
│ └── themes
├── project
│ ├── default_themes.json
│ └── naming_scheme_generator.py
├── shared_dependencies.md
└── tests
└── test_naming_scheme_generator.py

with themes folder likely intended for theme customization being empty and naming_scheme_generator.py containing unparsed GPT4 dialogue parts and the code inside referencing nonexistent theme_loader and name_generator:

Code for project/naming_scheme_generator.py:

```python
import random
import re
from theme_loader import load_theme_options
from name_generator import generate_name

def generate_naming_scheme():
theme_options = load_theme_options()
name = generate_name(theme_options)
while not re.match(r'^[a-z]_[a-z]+$', name):
name = generate_name(theme_options)
return name.replace('_', '-')

if __name__ == '__main__':
print(generate_naming_scheme())
```

This file imports the `load_theme_options` function from `theme_loader.py` and the `generate_name` function from `name_generator.py`. It defines a `generate_naming_scheme` function that generates a random name based on the loaded theme options and validates that the name is in the format of an adjective and a noun starting with the same letter separated by an underscore. Finally, it replaces the underscore with a hyphen and returns the generated name.

The `if __name__ == '__main__'` block is used for testing purposes and prints a generated name when the file is run as a script.

The test file also bears the same problem, likely an uncovered edge case in parsing the model output:

```
import pytest
from project.naming_scheme_generator import generate_name

def test_generate_name():
name = generate_name()
assert isinstance(name, str)
assert len(name.split()) == 2
assert name[0].lower() == name.split()[1][0].lower()
assert name.split()[0] in ['dramatic', 'melancholic', 'romantic', 'whimsical', 'nostalgic', 'mysterious', 'hopeful', 'futuristic', 'fantastical', 'enigmatic']
assert name.split()[1] in ['manifold', 'matrix', 'mystery', 'mirage', 'muse', 'machine', 'momentum', 'myth', 'maze', 'magnet']
```

Taking over at this point would be considerably more challenging than even starting from the first prompt we began with. This is something one has to be prepared for when managing automated AI development. Consistency of improvements between iterations is much more challenging to achieve than consistency within a single repository. At this point, the development route has hit a dead end — it would be unwise to continue from here. A possible course of action would be to revert to the previous step and either take over, as we’ve done above, or retry the self-improvement iteration.

Conclusions

Overall, the current progress in automated agent development can be seen as lightning-fast. Developing such a system with even minimal consistency in its behavior is no small feat.

The example we’ve shown might suggest a certain pattern in designing requirements for such an engine, which somewhat resembles the once widely used waterfall approach. This approach requires a detailed requirements and specifications document at the project kickoff, containing as many minute implementation details as possible.

This approach is going to be continually reinforced by growing context window of large language models.

This approach will likely be continually reinforced by the growing context window of large language models. On the other hand, transitioning to an iterative approach like classic agile or its variations may not be a trivial choice. Such an approach shifts significant weight from the initial design specifications to continuous interactions between different project members, product owners, and the end client. This falls within the scope of the automating agent, not the model itself, as is the case with increasing context windows.

While our coverage is limited in terms of functionality and product scope, the technical stack (being a dynamically typed language) and the task itself (requiring a combination of different types of content like code and static data) hopefully allow us to uncover potential pitfalls and gain useful insights.

Thank you for reading!

Join thousands of data leaders on the AI newsletter. Join over 80,000 subscribers and keep up to date with the latest developments in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.

Published via Towards AI

Feedback ↓

Sign Up for the Course
`; } else { console.error('Element with id="subscribe" not found within the page with class "home".'); } } }); // Remove duplicate text from articles /* Backup: 09/11/24 function removeDuplicateText() { const elements = document.querySelectorAll('h1, h2, h3, h4, h5, strong'); // Select the desired elements const seenTexts = new Set(); // A set to keep track of seen texts const tagCounters = {}; // Object to track instances of each tag elements.forEach(el => { const tagName = el.tagName.toLowerCase(); // Get the tag name (e.g., 'h1', 'h2', etc.) // Initialize a counter for each tag if not already done if (!tagCounters[tagName]) { tagCounters[tagName] = 0; } // Only process the first 10 elements of each tag type if (tagCounters[tagName] >= 2) { return; // Skip if the number of elements exceeds 10 } const text = el.textContent.trim(); // Get the text content const words = text.split(/\s+/); // Split the text into words if (words.length >= 4) { // Ensure at least 4 words const significantPart = words.slice(0, 5).join(' '); // Get first 5 words for matching // Check if the text (not the tag) has been seen before if (seenTexts.has(significantPart)) { // console.log('Duplicate found, removing:', el); // Log duplicate el.remove(); // Remove duplicate element } else { seenTexts.add(significantPart); // Add the text to the set } } tagCounters[tagName]++; // Increment the counter for this tag }); } removeDuplicateText(); */ // Remove duplicate text from articles function removeDuplicateText() { const elements = document.querySelectorAll('h1, h2, h3, h4, h5, strong'); // Select the desired elements const seenTexts = new Set(); // A set to keep track of seen texts const tagCounters = {}; // Object to track instances of each tag // List of classes to be excluded const excludedClasses = ['medium-author', 'post-widget-title']; elements.forEach(el => { // Skip elements with any of the excluded classes if (excludedClasses.some(cls => el.classList.contains(cls))) { return; // Skip this element if it has any of the excluded classes } const tagName = el.tagName.toLowerCase(); // Get the tag name (e.g., 'h1', 'h2', etc.) // Initialize a counter for each tag if not already done if (!tagCounters[tagName]) { tagCounters[tagName] = 0; } // Only process the first 10 elements of each tag type if (tagCounters[tagName] >= 10) { return; // Skip if the number of elements exceeds 10 } const text = el.textContent.trim(); // Get the text content const words = text.split(/\s+/); // Split the text into words if (words.length >= 4) { // Ensure at least 4 words const significantPart = words.slice(0, 5).join(' '); // Get first 5 words for matching // Check if the text (not the tag) has been seen before if (seenTexts.has(significantPart)) { // console.log('Duplicate found, removing:', el); // Log duplicate el.remove(); // Remove duplicate element } else { seenTexts.add(significantPart); // Add the text to the set } } tagCounters[tagName]++; // Increment the counter for this tag }); } removeDuplicateText(); //Remove unnecessary text in blog excerpts document.querySelectorAll('.blog p').forEach(function(paragraph) { // Replace the unwanted text pattern for each paragraph paragraph.innerHTML = paragraph.innerHTML .replace(/Author\(s\): [\w\s]+ Originally published on Towards AI\.?/g, '') // Removes 'Author(s): XYZ Originally published on Towards AI' .replace(/This member-only story is on us\. Upgrade to access all of Medium\./g, ''); // Removes 'This member-only story...' }); //Load ionic icons and cache them if ('localStorage' in window && window['localStorage'] !== null) { const cssLink = 'https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css'; const storedCss = localStorage.getItem('ionicons'); if (storedCss) { loadCSS(storedCss); } else { fetch(cssLink).then(response => response.text()).then(css => { localStorage.setItem('ionicons', css); loadCSS(css); }); } } function loadCSS(css) { const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); } //Remove elements from imported content automatically function removeStrongFromHeadings() { const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6, span'); elements.forEach(el => { const strongTags = el.querySelectorAll('strong'); strongTags.forEach(strongTag => { while (strongTag.firstChild) { strongTag.parentNode.insertBefore(strongTag.firstChild, strongTag); } strongTag.remove(); }); }); } removeStrongFromHeadings(); "use strict"; window.onload = () => { /* //This is an object for each category of subjects and in that there are kewords and link to the keywods let keywordsAndLinks = { //you can add more categories and define their keywords and add a link ds: { keywords: [ //you can add more keywords here they are detected and replaced with achor tag automatically 'data science', 'Data science', 'Data Science', 'data Science', 'DATA SCIENCE', ], //we will replace the linktext with the keyword later on in the code //you can easily change links for each category here //(include class="ml-link" and linktext) link: 'linktext', }, ml: { keywords: [ //Add more keywords 'machine learning', 'Machine learning', 'Machine Learning', 'machine Learning', 'MACHINE LEARNING', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, ai: { keywords: [ 'artificial intelligence', 'Artificial intelligence', 'Artificial Intelligence', 'artificial Intelligence', 'ARTIFICIAL INTELLIGENCE', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, nl: { keywords: [ 'NLP', 'nlp', 'natural language processing', 'Natural Language Processing', 'NATURAL LANGUAGE PROCESSING', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, des: { keywords: [ 'data engineering services', 'Data Engineering Services', 'DATA ENGINEERING SERVICES', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, td: { keywords: [ 'training data', 'Training Data', 'training Data', 'TRAINING DATA', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, ias: { keywords: [ 'image annotation services', 'Image annotation services', 'image Annotation services', 'image annotation Services', 'Image Annotation Services', 'IMAGE ANNOTATION SERVICES', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, l: { keywords: [ 'labeling', 'labelling', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, pbp: { keywords: [ 'previous blog posts', 'previous blog post', 'latest', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, mlc: { keywords: [ 'machine learning course', 'machine learning class', ], //Change your article link (include class="ml-link" and linktext) link: 'linktext', }, }; //Articles to skip let articleIdsToSkip = ['post-2651', 'post-3414', 'post-3540']; //keyword with its related achortag is recieved here along with article id function searchAndReplace(keyword, anchorTag, articleId) { //selects the h3 h4 and p tags that are inside of the article let content = document.querySelector(`#${articleId} .entry-content`); //replaces the "linktext" in achor tag with the keyword that will be searched and replaced let newLink = anchorTag.replace('linktext', keyword); //regular expression to search keyword var re = new RegExp('(' + keyword + ')', 'g'); //this replaces the keywords in h3 h4 and p tags content with achor tag content.innerHTML = content.innerHTML.replace(re, newLink); } function articleFilter(keyword, anchorTag) { //gets all the articles var articles = document.querySelectorAll('article'); //if its zero or less then there are no articles if (articles.length > 0) { for (let x = 0; x < articles.length; x++) { //articles to skip is an array in which there are ids of articles which should not get effected //if the current article's id is also in that array then do not call search and replace with its data if (!articleIdsToSkip.includes(articles[x].id)) { //search and replace is called on articles which should get effected searchAndReplace(keyword, anchorTag, articles[x].id, key); } else { console.log( `Cannot replace the keywords in article with id ${articles[x].id}` ); } } } else { console.log('No articles found.'); } } let key; //not part of script, added for (key in keywordsAndLinks) { //key is the object in keywords and links object i.e ds, ml, ai for (let i = 0; i < keywordsAndLinks[key].keywords.length; i++) { //keywordsAndLinks[key].keywords is the array of keywords for key (ds, ml, ai) //keywordsAndLinks[key].keywords[i] is the keyword and keywordsAndLinks[key].link is the link //keyword and link is sent to searchreplace where it is then replaced using regular expression and replace function articleFilter( keywordsAndLinks[key].keywords[i], keywordsAndLinks[key].link ); } } function cleanLinks() { // (making smal functions is for DRY) this function gets the links and only keeps the first 2 and from the rest removes the anchor tag and replaces it with its text function removeLinks(links) { if (links.length > 1) { for (let i = 2; i < links.length; i++) { links[i].outerHTML = links[i].textContent; } } } //arrays which will contain all the achor tags found with the class (ds-link, ml-link, ailink) in each article inserted using search and replace let dslinks; let mllinks; let ailinks; let nllinks; let deslinks; let tdlinks; let iaslinks; let llinks; let pbplinks; let mlclinks; const content = document.querySelectorAll('article'); //all articles content.forEach((c) => { //to skip the articles with specific ids if (!articleIdsToSkip.includes(c.id)) { //getting all the anchor tags in each article one by one dslinks = document.querySelectorAll(`#${c.id} .entry-content a.ds-link`); mllinks = document.querySelectorAll(`#${c.id} .entry-content a.ml-link`); ailinks = document.querySelectorAll(`#${c.id} .entry-content a.ai-link`); nllinks = document.querySelectorAll(`#${c.id} .entry-content a.ntrl-link`); deslinks = document.querySelectorAll(`#${c.id} .entry-content a.des-link`); tdlinks = document.querySelectorAll(`#${c.id} .entry-content a.td-link`); iaslinks = document.querySelectorAll(`#${c.id} .entry-content a.ias-link`); mlclinks = document.querySelectorAll(`#${c.id} .entry-content a.mlc-link`); llinks = document.querySelectorAll(`#${c.id} .entry-content a.l-link`); pbplinks = document.querySelectorAll(`#${c.id} .entry-content a.pbp-link`); //sending the anchor tags list of each article one by one to remove extra anchor tags removeLinks(dslinks); removeLinks(mllinks); removeLinks(ailinks); removeLinks(nllinks); removeLinks(deslinks); removeLinks(tdlinks); removeLinks(iaslinks); removeLinks(mlclinks); removeLinks(llinks); removeLinks(pbplinks); } }); } //To remove extra achor tags of each category (ds, ml, ai) and only have 2 of each category per article cleanLinks(); */ //Recommended Articles var ctaLinks = [ /* ' ' + '

Subscribe to our AI newsletter!

' + */ '

Take our 85+ lesson From Beginner to Advanced LLM Developer Certification: From choosing a project to deploying a working product this is the most comprehensive and practical LLM course out there!

'+ '

Towards AI has published Building LLMs for Production—our 470+ page guide to mastering LLMs with practical projects and expert insights!

' + '
' + '' + '' + '

Note: Content contains the views of the contributing authors and not Towards AI.
Disclosure: This website may contain sponsored content and affiliate links.

' + 'Discover Your Dream AI Career at Towards AI Jobs' + '

Towards AI has built a jobs board tailored specifically to Machine Learning and Data Science Jobs and Skills. Our software searches for live AI jobs each hour, labels and categorises them and makes them easily searchable. Explore over 10,000 live jobs today with Towards AI Jobs!

' + '
' + '

🔥 Recommended Articles 🔥

' + 'Why Become an LLM Developer? Launching Towards AI’s New One-Stop Conversion Course'+ 'Testing Launchpad.sh: A Container-based GPU Cloud for Inference and Fine-tuning'+ 'The Top 13 AI-Powered CRM Platforms
' + 'Top 11 AI Call Center Software for 2024
' + 'Learn Prompting 101—Prompt Engineering Course
' + 'Explore Leading Cloud Providers for GPU-Powered LLM Training
' + 'Best AI Communities for Artificial Intelligence Enthusiasts
' + 'Best Workstations for Deep Learning
' + 'Best Laptops for Deep Learning
' + 'Best Machine Learning Books
' + 'Machine Learning Algorithms
' + 'Neural Networks Tutorial
' + 'Best Public Datasets for Machine Learning
' + 'Neural Network Types
' + 'NLP Tutorial
' + 'Best Data Science Books
' + 'Monte Carlo Simulation Tutorial
' + 'Recommender System Tutorial
' + 'Linear Algebra for Deep Learning Tutorial
' + 'Google Colab Introduction
' + 'Decision Trees in Machine Learning
' + 'Principal Component Analysis (PCA) Tutorial
' + 'Linear Regression from Zero to Hero
'+ '

', /* + '

Join thousands of data leaders on the AI newsletter. It’s free, we don’t spam, and we never share your email address. Keep up to date with the latest work in AI. From research to projects and ideas. If you are building an AI startup, an AI-related product, or a service, we invite you to consider becoming a sponsor.

',*/ ]; var replaceText = { '': '', '': '', '
': '
' + ctaLinks + '
', }; Object.keys(replaceText).forEach((txtorig) => { //txtorig is the key in replacetext object const txtnew = replaceText[txtorig]; //txtnew is the value of the key in replacetext object let entryFooter = document.querySelector('article .entry-footer'); if (document.querySelectorAll('.single-post').length > 0) { //console.log('Article found.'); const text = entryFooter.innerHTML; entryFooter.innerHTML = text.replace(txtorig, txtnew); } else { // console.log('Article not found.'); //removing comment 09/04/24 } }); var css = document.createElement('style'); css.type = 'text/css'; css.innerHTML = '.post-tags { display:none !important } .article-cta a { font-size: 18px; }'; document.body.appendChild(css); //Extra //This function adds some accessibility needs to the site. function addAlly() { // In this function JQuery is replaced with vanilla javascript functions const imgCont = document.querySelector('.uw-imgcont'); imgCont.setAttribute('aria-label', 'AI news, latest developments'); imgCont.title = 'AI news, latest developments'; imgCont.rel = 'noopener'; document.querySelector('.page-mobile-menu-logo a').title = 'Towards AI Home'; document.querySelector('a.social-link').rel = 'noopener'; document.querySelector('a.uw-text').rel = 'noopener'; document.querySelector('a.uw-w-branding').rel = 'noopener'; document.querySelector('.blog h2.heading').innerHTML = 'Publication'; const popupSearch = document.querySelector$('a.btn-open-popup-search'); popupSearch.setAttribute('role', 'button'); popupSearch.title = 'Search'; const searchClose = document.querySelector('a.popup-search-close'); searchClose.setAttribute('role', 'button'); searchClose.title = 'Close search page'; // document // .querySelector('a.btn-open-popup-search') // .setAttribute( // 'href', // 'https://medium.com/towards-artificial-intelligence/search' // ); } // Add external attributes to 302 sticky and editorial links function extLink() { // Sticky 302 links, this fuction opens the link we send to Medium on a new tab and adds a "noopener" rel to them var stickyLinks = document.querySelectorAll('.grid-item.sticky a'); for (var i = 0; i < stickyLinks.length; i++) { /* stickyLinks[i].setAttribute('target', '_blank'); stickyLinks[i].setAttribute('rel', 'noopener'); */ } // Editorial 302 links, same here var editLinks = document.querySelectorAll( '.grid-item.category-editorial a' ); for (var i = 0; i < editLinks.length; i++) { editLinks[i].setAttribute('target', '_blank'); editLinks[i].setAttribute('rel', 'noopener'); } } // Add current year to copyright notices document.getElementById( 'js-current-year' ).textContent = new Date().getFullYear(); // Call functions after page load extLink(); //addAlly(); setTimeout(function() { //addAlly(); //ideally we should only need to run it once ↑ }, 5000); }; function closeCookieDialog (){ document.getElementById("cookie-consent").style.display = "none"; return false; } setTimeout ( function () { closeCookieDialog(); }, 15000); console.log(`%c 🚀🚀🚀 ███ █████ ███████ █████████ ███████████ █████████████ ███████████████ ███████ ███████ ███████ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Towards AI is looking for contributors! │ │ Join us in creating awesome AI content. │ │ Let's build the future of AI together → │ │ https://towardsai.net/contribute │ │ │ └───────────────────────────────────────────────────────────────────┘ `, `background: ; color: #00adff; font-size: large`); //Remove latest category across site document.querySelectorAll('a[rel="category tag"]').forEach(function(el) { if (el.textContent.trim() === 'Latest') { // Remove the two consecutive spaces (  ) if (el.nextSibling && el.nextSibling.nodeValue.includes('\u00A0\u00A0')) { el.nextSibling.nodeValue = ''; // Remove the spaces } el.style.display = 'none'; // Hide the element } }); // Add cross-domain measurement, anonymize IPs 'use strict'; //var ga = gtag; ga('config', 'G-9D3HKKFV1Q', 'auto', { /*'allowLinker': true,*/ 'anonymize_ip': true/*, 'linker': { 'domains': [ 'medium.com/towards-artificial-intelligence', 'datasets.towardsai.net', 'rss.towardsai.net', 'feed.towardsai.net', 'contribute.towardsai.net', 'members.towardsai.net', 'pub.towardsai.net', 'news.towardsai.net' ] } */ }); ga('send', 'pageview'); -->